UVM基础总结——基于《UVM实战》示例代码--deepseek

UVM基础总结——基于《UVM实战》示例
https://www.cnblogs.com/moluoqishi/p/14289288.html
这篇文章很好,关于uvm的,可是我目前还看不懂里面的代码,你可以带我一起看看里面的代码,给我讲讲嘛,老师谢谢您
同学,坐。看到你主动去啃博客里的代码了,老师特别高兴。代码看不懂没关系,咱们今天就把它当成一个“剧本”,一句一句地读。
你之前对UVM的结构已经有了很好的理解,现在我们把这些结构对应到具体的代码上。老师会继续用 “剧组拍电影” 的比喻,带着你从这堆代码里,把每个角色的台词都认一遍。
🎬 第一幕:道具组——transaction(子弹/剧本)
你看文章里最先定义的 my_transaction,它继承了 uvm_sequence_item。这就像你Verilog里定义的一组信号线,但它更高级,是一个类。
systemverilog
class my_transaction extends uvm_sequence_item;
rand bit[47:0] dmac;
rand bit[47:0] smac;
rand bit[15:0] ether_type;
rand byte pload[];
rand bit[31:0] crc;
// ...
constraint pload_cons{
pload.size >= 46;
pload.size <= 200;
dmac == 48'h01_02_03_04_05_06;
smac == 48'h60_50_40_30_20_10;
}
endclass
-
比喻:这就是一颗子弹(或者说一页剧本)。
dmac、smac是源MAC地址和目的MAC地址,pload是数据载荷。rand关键字告诉UVM:“每次生成子弹时,这些字段可以随机变化”。 -
constraint:这是约束条件。它保证子弹不是乱造的,比如数据长度必须在46到200之间,目的地址必须固定。这就像导演要求:“枪战戏的子弹必须是空包弹,不能是真子弹”。 -
和你熟悉的知识对照:你以前在Verilog里手动给
data_in赋值,现在UVM帮你自动“随机化”这颗子弹,但同时在约束里画好了“安全框”。
🎬 第二幕:弹药手——sequence(弹夹)
my_sequence 继承了 uvm_sequence,它负责装弹和决定什么时候开火。
systemverilog
class my_sequence extends uvm_sequence #(my_transaction);
my_transaction m_trans;
virtual task body();
if(starting_phase != null)
starting_phase.raise_objection(this);
repeat(10) begin
`uvm_do(m_trans)
end
#1000;
if(starting_phase != null)
starting_phase.drop_objection(this);
endtask
endclass
-
比喻:这是一个弹夹。
body()任务就是弹夹的“装填和射击流程”。 -
raise_objection/drop_objection:还记得我们上次课讲的“反对票”吗?raise_objection(this)就是在喊:“等等!我还没打完,别关仿真!”。drop_objection(this)是在喊:“我打完了,可以收工了!”。 -
repeat(10) beginuvm_do(m_trans) end**:这是“连发模式”。uvm_do` 是一个宏,它会自动完成“创建一颗子弹(transaction)、随机化它、然后把它交给手枪(sequencer)”这一整套动作。所以这里的意思是:连续打10发子弹**。 -
#1000:打完10发后,再等1000个时间单位。这就像导演喊:“卡!让演员们喘口气,1000个时钟周期后我们再收工。” 这就是objection机制在代码里的实际体现:只要sequence还在raise状态,仿真时间就继续往前走。
🎬 第三幕:手枪与扳机——sequencer 和 driver
my_sequencer 几乎是个空壳,它只是继承了 uvm_sequencer 并参数化为 my_transaction 类型。
systemverilog
class my_sequencer extends uvm_sequencer #(my_transaction);
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
`uvm_component_utils(my_sequencer)
endclass
-
比喻:这就是手枪本身。它自己不会造子弹,也不会扣扳机,它的作用就是连接弹夹(sequence)和扳机(driver),充当一个中转站。
而 my_driver 才是真正干活的 “扳机” 。
systemverilog
class my_driver extends uvm_driver#(my_transaction);
virtual my_if vif;
`uvm_component_utils(my_driver)
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
`uvm_fatal("my_driver", "virtual interface must be set for vif!!!")
endfunction
task my_driver::main_phase(uvm_phase phase);
vif.data <= 8'b0;
vif.valid <= 1'b0;
while(!vif.rst_n)
@(posedge vif.clk);
while(1)begin
seq_item_port.get_next_item(req);
drive_one_pkt(req);
seq_item_port.item_done();
end
endtask
endclass
-
virtual my_if vif:这是虚接口。你熟悉FPGA,知道DUT的引脚是通过interface定义的。driver需要拿到这个接口才能操作信号线。 -
build_phase里的uvm_config_db:这是UVM的资源传递机制。它就像剧组的物资分发处,把input_if这个接口,从顶层传递到my_driver手里。如果没拿到,就直接uvm_fatal报错——连枪都没有,这戏没法拍了。 -
main_phase里的while(1)循环:这是driver的核心生命线。-
seq_item_port.get_next_item(req):这是driver在问手枪:“下一发子弹装好了吗?” 如果弹夹里没子弹,driver就会在这里阻塞等待,直到sequence把新的子弹放进来。 -
drive_one_pkt(req):拿到子弹后,按照协议时序,把子弹“打”进DUT。这个任务的具体内容,就是你在Verilog里写的那些@(posedge clk); vif.data <= ...; vif.valid <= 1'b1;之类的时序操作。 -
seq_item_port.item_done():告诉手枪:“这发子弹我打完了,可以装下一发了”。这个item_done和sequence里的uvm_do是配对的,只有driver调用它,sequence才会继续发下一颗子弹。
-
-
和你熟悉的知识对照:这就是把你Verilog里
initial begin ... end的激励生成过程,变成了一个永不停止的循环,并且把“从队列取数据”和“驱动信号”分开了。
🎬 第四幕:摄像机——monitor(在文章中未给出完整代码)
虽然文章没有贴出 my_monitor 的完整代码,但根据UVM的标准结构和你的图,我们可以推断出它的核心逻辑:
systemverilog
class my_monitor extends uvm_monitor;
virtual my_if vif;
uvm_analysis_port #(my_transaction) ap; // 分析端口
virtual task main_phase(uvm_phase phase);
my_transaction tr;
forever begin
@(posedge vif.clk);
if (vif.valid) begin // 检测到有效数据
tr = my_transaction::type_id::create("tr");
tr.data = vif.data; // 采集信号
ap.write(tr); // 发送给scoreboard
end
end
endtask
endclass
-
比喻:这就是摄像机。它不干涉拍摄,只是在旁边静静地记录。
@(posedge vif.clk)和if (vif.valid)就是在等待有效信号,一旦DUT有数据输出,就立刻采集下来,打包成一颗“子弹”,通过ap.write(tr)这个“分析端口”发射出去。
🎬 第五幕:裁判——scoreboard
my_scoreboard 是最后进行自动比对的地方。
systemverilog
while(expect_queue.size() > 0) begin
tmp_tran = expect_queue.pop_front();
result = get_actual.compare(tmp_tran);
if(result) begin
`uvm_info("my_scoreboard", "Compare SUCCESSFULLY", UVM_LOW);
end
else begin
`uvm_error("my_scoreboard", "Compare FAILED");
end
end
-
比喻:这是裁判。它手里拿着两个队列:一个是
expect_queue(期望结果队列,由reference model提前算好放进去的),另一个是get_actual(实际结果,由out_agent的monitor采集送来的)。 -
pop_front()和compare():裁判从期望队列里拿出一份标准答案,和实际收到的结果进行逐字段比对。 -
uvm_info和uvm_error:如果比对成功,打印 "Compare SUCCESSFULLY"(通过)。如果失败,打印 "Compare FAILED"(失败),并且会记录错误数量,在最后base_test的report_phase里决定整个测试用例是 "TEST CASE PASSED" 还是 "TEST CASE FAILED"。
🎬 总结:完整的剧组一天
你看,把这些代码片段串起来,就是一个完整的自动化测试流水线:
-
顶层
top_tb:这是剧场,里面有dut(真正的硬件)、clk时钟、rst_n复位,以及通过uvm_config_db分发下去的input_if和output_if两个接口。 -
my_case0:这是总导演。它的build_phase里,通过uvm_config_db告诉sequencer:“今天拍这场戏,用case0_sequence这个剧本”。 -
case0_sequence:剧本启动。body()任务开始执行,它先raise_objection告诉仿真器:“我要开始拍戏了,别停”。 -
循环:sequence 连续10次通过
uvm_do把子弹(transaction)装进手枪(sequencer)。 -
driver:在
main_phase的while(1)里,不停地从手枪里取子弹(get_next_item),然后按照以太网协议,把数据一位一位地打到input_if这个接口上,再通过接口传给DUT。 -
monitor:摄像机在
output_if上盯着,一旦DUT吐出数据,就抓下来,打包好,通过analysis_port发给scoreboard。 -
scoreboard:裁判拿到monitor传来的实际数据,和reference model算出的期望数据进行比对,告诉你是成功还是失败。
-
sequence打完10发,
#1000延时结束,drop_objection撤销反对票。所有objection被清除,run_phase结束,仿真进入report_phase。 -
base_test::report_phase:总导演查看uvm_report_server,如果错误数为0,大喊 "TEST CASE PASSED";否则,大喊 "TEST CASE FAILED"。
同学,你看,代码虽然长,但每一行都在为你“打工”。你不用一次性全记住,今天能把这些“零件”和它们对应的“功能”对上号,就已经非常棒了。就像你设计FPGA时,先看顶层框图,再深入每个模块。读UVM代码也是同理,先抓住 sequence → sequencer → driver → DUT → monitor → scoreboard 这条主线,细节可以慢慢品。
今天先消化到这。如果还有哪个片段觉得绕,随时截取发过来,咱们继续“一句一句过”。慢慢来,老师陪着你。
更多推荐


所有评论(0)