3、Transaction级调试:利用 do_print/do_record/do_copy 方法打印和记录事务、在波形中查看 transaction 信息
3.1 为什么需要 Transaction 级调试?
做验证的兄弟都知道,信号级的调试太痛苦了。你盯着波形里那些 0 和 1 的跳变,眼睛都快看瞎了,还是搞不清数据对不对。我刚开始做 UVM 那会儿,就吃过这个亏。一个 AXI 总线的事务,几百个信号在波形里翻来覆去,查一个地址错误查了整整两天。
后来我才明白,我们真正关心的不是信号怎么跳,而是事务对不对。说白了,你发了一个写请求,地址是 0x1000,数据是 0xA5A5A5A5,你只想知道这个事务有没有被正确发送和接收。至于它是在哪个时钟沿采样的,那是工具该关心的事。
UVM 给我们提供了三个好用的方法:do_print、do_record 和 do_copy。用好它们,调试效率能翻好几倍。
核心思想:把信号级的调试提升到事务级。你看到的不再是波形里的 0 和 1,而是有意义的 transaction 内容。
3.2 do_print:让事务自己说话
do_print 是 UVM 中最常用的调试方法。它能把一个 transaction 对象的内容,以可读的格式打印出来。我习惯在 monitor 或者 scoreboard 里调用它,每次抓到新事务就打印一下,看看数据对不对。
先看一个简单的例子:
class my_transaction extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit wr_en; // 1: write, 0: read
`uvm_object_utils_begin(my_transaction)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_DEFAULT)
`uvm_field_int(wr_en, UVM_DEFAULT)
`uvm_object_utils_end
// 自定义打印格式
function void do_print(uvm_printer printer);
super.do_print(printer);
printer.print_string("type", wr_en ? "WRITE" : "READ");
printer.print_hex("address", addr, 32);
printer.print_hex("data", data, 32);
endfunction
endclass
这样打印出来的结果,一眼就能看懂:
--------------------------------------------------------------
Name Type Size Value
--------------------------------------------------------------
type string - WRITE
address hex 32 0x00001000
data hex 32 0xA5A5A5A5
--------------------------------------------------------------
嗯,这里要注意:do_print 默认会打印所有用 uvm_field_* 注册的字段。但有时候你只想看关键信息,那就自己重写 do_print,只打印你关心的内容。
我的习惯:在 do_print 里加一个时间戳。这样看 log 的时候,能清楚知道每个事务发生的时刻。比如:printer.print_time("timestamp", $time);
3.3 do_record:把事务写进波形
说实话,do_record 是我最喜欢的调试方法。它能把 transaction 的信息直接写到波形文件里。你想想看,在波形里直接看到 "WRITE 0x1000 0xA5A5A5A5" 这样的信息,比看一堆信号线舒服多了。
实现起来也很简单:
class my_transaction extends uvm_sequence_item;
// ... 前面的定义不变 ...
function void do_record(uvm_recorder recorder);
super.do_record(recorder);
recorder.record_string("type", wr_en ? "WRITE" : "READ");
recorder.record_field("address", addr, 32);
recorder.record_field("data", data, 32);
endfunction
endclass
然后在你的 agent 或者 monitor 里,打开记录功能:
class my_monitor extends uvm_monitor;
virtual task run_phase(uvm_phase phase);
my_transaction tr;
forever begin
// 从接口抓取事务
tr = my_transaction::type_id::create("tr");
// ... 填充事务内容 ...
// 记录到波形
void'(tr.begin_tr());
tr.do_record(tr.get_recorder());
tr.end_tr();
// 同时发送给 scoreboard
ap.write(tr);
end
endtask
endclass
打开波形后,你会看到类似这样的信息:
# 在波形的事务栏里显示:
# [100ns] WRITE addr=0x1000 data=0xA5A5A5A5
# [200ns] READ addr=0x1000 data=0x00000000
我曾经踩过的坑:begin_tr 和 end_tr 必须成对出现。少了一个,波形里的事务信息就会乱掉。而且 begin_tr 会返回一个时间戳,你可以用它来标记事务的开始时间。
3.4 do_copy:深拷贝的正确姿势
调试的时候,经常需要复制事务对象。比如 scoreboard 里要保存一个 reference 队列,或者 monitor 里要缓存最近几个事务。这时候 do_copy 就派上用场了。
默认的 do_copy 只会复制用 uvm_field_* 注册的字段。但如果你有动态数组、队列或者自定义对象,就得自己重写:
class my_transaction extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit wr_en;
byte payload[]; // 动态数组,需要手动复制
`uvm_object_utils_begin(my_transaction)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_DEFAULT)
`uvm_field_int(wr_en, UVM_DEFAULT)
`uvm_object_utils_end
function void do_copy(uvm_object rhs);
my_transaction tr;
super.do_copy(rhs);
// 类型检查,确保安全
if (!$cast(tr, rhs)) begin
`uvm_fatal("COPY", "Type mismatch in do_copy")
end
// 复制动态数组
payload = new[tr.payload.size()];
foreach (tr.payload[i]) begin
payload[i] = tr.payload[i];
end
endfunction
endclass
为什么要自己写 do_copy?因为 UVM 的自动复制只处理简单类型。动态数组、关联数组、队列这些,它不会帮你深拷贝。我遇到过好几次,复制后两个对象共享同一个数组,改了一个另一个也跟着变,调试起来一头雾水。
避坑指南:如果你在 transaction 里用了 randc 变量,复制后记得重新随机化。否则复制出来的对象和原对象共享同一个随机状态,可能会出问题。
3.5 知识体系总览
下面这张图,是我自己总结的 Transaction 级调试的核心逻辑。你看一眼就能明白这三个方法的关系:
3.6 实战建议
说了这么多,我总结几条实战经验:
- 先打印,后波形:调试初期先用
do_print快速定位问题。等找到可疑事务后,再用do_record把相关事务写到波形里,结合时序分析。 - 控制打印频率:别每个事务都打印。仿真跑几百万个事务,打印文件会大到爆炸。我一般只在第一次抓到事务、或者检测到错误时才打印。
- do_copy 要谨慎:复制大对象时注意性能。如果只是临时查看,用
do_print就够了,没必要复制一份。 - 统一格式:团队协作时,大家约定好
do_print和do_record的输出格式。这样看 log 和波形时,不会因为格式不统一而浪费时间。
小技巧:你可以在 do_print 里加一个条件判断。比如只在 wr_en == 1 时打印详细信息,读操作只打印地址。这样 log 文件不会太臃肿,关键信息又不会漏掉。
好了,Transaction 级调试就聊到这儿。这三个方法用好了,调试效率上一个台阶。下次你遇到 bug,不妨先试试用 do_print 看看事务内容,说不定一眼就能发现问题。