读研那会儿我第一次接触AXI验证,环境里前辈给了一套Synopsys AXI VIP,但我根本不知道它连port monitor都已经帮我铺好了。每天上班就是开Verdi拖波形,AWADDR拉一行、WDATA拉一行,对着spec一条条手动核对,晚上加班到九点是常事。后来我把这套用Port Monitor自动收事务、直接连Scoreboard的路子跑通之后,才体会到什么叫“验证工程师该干的活”——你不需要每天重复“肉眼看波形”,而是让环境自己告诉你哪里不对。
这篇文章我就把完整做法拆开讲,包括Port Monitor到底监控了什么、怎么把它和UVM Scoreboard连接、连接代码怎么写,以及我实测踩过的几个坑。适合已经在用UVM和AXI VIP、但还没有把事务级自动比对跑起来的朋友。无论你用的是SVT AXI VIP还是自封装的AXI agent,思路都一样。
1. 为什么你一听到“手动抓波形”就想换工作
1.1 波形级调试的三个真实代价
手动抓波形最大的问题不是“累”,而是“不可靠”。AWADDR和AWREADY都对上,你以为握手成功了,但WVALID的采样沿没对齐,最后data其实晚了一个周期才被收走。这种问题在波形上非常隐蔽,靠眼睛刷屏幕很容易漏掉。更麻烦的是,你花一小时整理出来的Excel比对表,下一次回归可能就变了,全部得重做。
第二个代价是效率。一个AXI4接口一个通道的事务,可能一秒钟有几千笔。真实SoC验证环境里,DDR读写+总线仲裁+外设访问混在一起,你根本不可能靠手动把每一笔事务的时间戳、地址、数据长度、响应全部记录完整。人工采样永远是抽样,抽样就意味着盲区。
第三个代价是回归不可追溯。你手动比对的东西,无法沉淀成自动化脚本或者断言。今天你不小心看错了一个BURST边界,明天就不会有人发现。DV环境里最忌讳的就是“这次靠人眼确认没问题”,因为验证的目的不是证明“这次能跑通”,而是保证“每次改动之后永远能跑通”。
1.2 从信号级上升到事务级,是DV的必经之路
AXI这类协议的特点是握手信号和事务边界分离。AWVALID和AWREADY都拉高了,这只是一次地址握手完成;真正意义上的“一次写事务”要到B通道响应回来才结束。你在波形层看到的是离散的时序,在事务层看到的才是完整的地址、数据、响应、ID组合。Port Monitor的本质,就是帮你在握手信号之间做聚合,把信号级的波形翻译成事务级的数据对象。这个对象可以直接扔给Scoreboard,和reference model或者预生成的期望值做比对。
我见过不少团队,手写monitor时从波形里抠地址、抠数据、拼接通道,一是工作量大,二是很容易出现边界判断错误。很多AXI VIP都已经把这些脏活干完了,只是很多人没有去用它的port monitor而已。
2. Synopsys AXI VIP的Port Monitor到底监控了什么
2.1 Port Monitor在VIP架构里的位置
Synopsys的AXI VIP通常是基于SVT(Smart Verification Technology)的,整体架构里会有一个类似svt_axi_system的顶层容器,下面放着master、slave、配置对象和监控组件。我习惯把它理解成三个层次:协议层负责驱动和采样信号;事务层负责把信号变成svt_axi_transaction;应用层才是我们写testbench的人真正打交道的地方。Port Monitor就横跨事务层和应用层。
Port Monitor是一个从端口角度观察总线活动的组件。什么叫“端口角度”?AXI总线里有master和slave,每个master或者slave的端口上,读写通道都是独立的。Port Monitor会同时盯住AW通道、W通道、B通道、AR通道、R通道,等这组通道上的握手完成后,组装出一个完整的事务对象。
要注意,Port Monitor和协议检查器(protocol checker)不是一回事。协议检查器负责报协议违例,比如AWADDR没有对齐、BURST长度超过限制;Port Monitor负责把“合法的事务”收集并广播出来,供Scoreboard、覆盖率模型、参考模型使用。理解这个分工,你才知道该去哪里接自己的逻辑。
2.2 它产生的事务对象带来哪些信息
事务对象里通常包含这些关键信息:地址(addr)、突发类型(burst)、突发长度(burst length)、数据宽度(size)、事务ID、读写方向、数据内容、响应类型,以及必要的时序标记。这些信息比你从波形里手动抠出来的要完整得多。
举个例子,你从波形上看一次写事务,至少要关注AWADDR、AWSIZE、AWLEN、WVALID、WREADY、WDATA、WLAST、BVALID、BREADY、BRESP这些信号的变化。手动对着它们判断事务边界,写三个if块都不一定够。Port Monitor内部严格按照AXI协议状态机帮你做了握手跟踪,事务完成时直接给你一个完整对象,你只需要在write回调里消费它。
还有个很实用的点:很多AXI VIP的port monitor可以配置为“在事务开始时也发一次”或者“只在事务完成时发一次”。我建议默认只在完成时发,这样Scoreboard比对逻辑更干净。
2.3 怎么确认自己已经打开了Port Monitor
不同版本的Synopsys AXI VIP,监控特性的开启方式不完全一样。有些版本里是配置svt_axi_port_configuration下的监控使能位,有些版本是在svt_axi_system’sconfiguration里打开port_monitor_enable,还有的版本默认就是开的。不要指望我写一个开关名你就能直接抄,我给你的实用建议是:去VIP包的头文件里grep这几个关键词——
grep -ri "port_monitor_enable" $SVTVIP_HOME grep -ri "monitor_enable" $SVTVIP_HOME grep -ri "uvm_analysis_port" $SVTVIP_HOME看到有哪些配置项和你用的VIP版本匹配,再对照User Guide确认。最直接的验证方法是:在testbench里打印uvm_top.print_topology(),看看环境树里是否出现了类似于svt_axi_port_monitor、svt_axi_system_monitor之类的组件。如果树上有这个组件但你没看到事务输出,多半是它的analysis port没有连接,或者monitor的verbosity被设成了不打印。
3. 把Port Monitor和Scoreboard接起来:UVM数据流怎么走
3.1 analysis port这条路才是正确的
很多初学UVM的人看到Port Monitor,第一反应是直接调用它的公共方法或者拿一个队列去存它的transaction。这么做不是不行,但会破坏UVM的组件通信模型。正确做法是:Port Monitor在事务结束时往自己的analysis port上调用write(tr),我们用一个analysis imp的scoreboard作为订阅方,在write函数里接收事务并处理。
为什么必须走analysis port?因为analysis port是广播机制,同一份数据可以被多个订阅者消费,而不用关心订阅者是谁。你可能希望Scoreboard收到所有事务做比对,又希望覆盖率模型收到同样的事务做功能覆盖率,还希望一个延迟统计模块收到带时间戳的事务。如果都靠直接调用,monitor就得知道每一个下游组件的类型,耦合度灾难。
如果你用的是UVM的uvm_analysis_imp,scoreboard的write函数签名是这样的:
virtual function void write(svt_axi_transaction tr); endfunction这个write是阻塞函数,理论上会慢慢执行。但对仿真来讲没有问题,因为Scoreboard只是消费数据,不产生新的协议行为。
3.2 scoreboard端用什么卡口收数据
UVM里有uvm_subscriber和uvm_analysis_imp两种常见接法。如果你的scoreboard只需要“收事务然后比对”,可以直接继承uvm_scoreboard,然后声明一个uvm_analysis_imp #(svt_axi_transaction, axi_scoreboard)类型的端口;如果你的环境里还有reference model,可能需要两条数据流,那么可以用uvm_analysis_imp_decl宏生成不同名字的imp。
我用uvm_analysis_imp_decl比较多,因为它能区分“monitor来的事务”和“reference model来的期望事务”,避免都挤进同一个write里。下面我会给一个使用这个声明的完整例子,你可以直接改改移植到自己环境里。
要注意的一点:uvm_analysis_imp本质上是一个parameterized class,它的第二个参数是下游组件的类型。因为这个机制,write函数会被UVM自动调用,而你不必在scoreboard里手动new一个port再connect,只需要在build_phase里创建imp,然后在env的connect_phase里调用connect。
4. 完整连接例码:从监控器到计分板一次跑通
4.1 scoreboard怎么写
下面这段代码是我在一个ARM-like验证环境里跑通过的简化版本。它做的事情是:接收Port Monitor发来的svt_axi_transaction,根据读写方向做计数,并做最基础的数据比对(这里省去了reference model的期望队列,实际项目里还会有更复杂的比对)。
// axi_traffic_scoreboard.sv `uvm_analysis_imp_decl(_mon) `uvm_analysis_imp_decl(_ref) class axi_traffic_scoreboard extends uvm_scoreboard; `uvm_component_utils(axi_traffic_scoreboard) uvm_analysis_imp_mon #(svt_axi_transaction, axi_traffic_scoreboard) mon_export; uvm_analysis_imp_ref #(svt_axi_transaction, axi_traffic_scoreboard) ref_export; int wr_cnt; int rd_cnt; int err_cnt; function new(string name = "axi_traffic_scoreboard", uvm_component parent = null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); mon_export = new("mon_export", this); ref_export = new("ref_export", this); endfunction virtual function void write_mon(svt_axi_transaction tr); // 这里的tr就是Port Monitor广播出来的事务对象 if (tr.xact_type == svt_axi_transaction::WRITE) begin wr_cnt++; // 实际项目里会进一步取tr.data,比对期望值 end else if (tr.xact_type == svt_axi_transaction::READ) begin rd_cnt++; end else begin `uvm_error(get_type_name(), "unexpected transaction type") end endfunction virtual function void write_ref(svt_axi_transaction tr); // 如果后续加了reference model,在这里把期望事务存入队列 endfunction endclass这段代码里我并没有写完整的数据比对队列,因为不同项目的reference model差异很大。但你已经可以看到核心机制:write_mon会在每一笔辅助监视器事务产生时自动被调用,你在这里计数、比对、报错都行。很多场景下,你甚至不需要自己写svt_axi_transaction的字段解析,只要对着老协议从tr.addr、tr.burst_length、tr.data访问即可。
4.2 env里怎么连
连接才是最关键的。假设你有一个AXI agent,内部包了一个port monitor并暴露了analysis port:mon_ap。在env的connect_phase里把这个port和scoreboard的imp连起来:
class axi_env extends uvm_env; `uvm_component_utils(axi_env) axi_master_agent mst_agent; axi_traffic_scoreboard scb; function new(string name = "axi_env", uvm_component parent = null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); mst_agent = axi_master_agent::type_id::create("mst_agent", this); scb = axi_traffic_scoreboard::type_id::create("scb", this); endfunction function void connect_phase(uvm_phase phase); super.connect_phase(phase); // 把Port Monitor的analysis port接到scoreboard的mon_export mst_agent.mon_ap.connect(scb.mon_export); endfunction endclass如果你的VIP没有直接暴露mon_ap,可以用一个包装subscriber来中转:
class axi_monitor_bridge extends uvm_subscriber #(svt_axi_transaction); `uvm_component_utils(axi_monitor_bridge) uvm_analysis_port #(svt_axi_transaction) mon_ap; function new(string name = "axi_monitor_bridge", uvm_component parent = null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); mon_ap = new("mon_ap", this); endfunction virtual function void write(svt_axi_transaction tr); mon_ap.write(tr); endfunction endclass然后你在env里把VIP内部monitor的analysis port连到bridge的analysis imp,再把bridge的mon_ap连到scoreboard。这多一层虽然看起来绕,但能让你在数据路径上随便插filter、插delay、插统计模块,调试起来特别方便。
4.3 按读写通道拆开统计的过滤器
一个AXI4接口,master读DDR、写DDR、slave响应等,可能好几条通道同时有事务。Scoreboard最好能按类型拆开统计,这样定位问题会更快。最常见的做法是在write_mon里直接过滤,或者用一个专门的axi_txn_filter组件,按通道输出到不同的analysis port。我用得比较多的是后者,因为过滤逻辑可以复用:
class axi_txn_filter extends uvm_component; `uvm_component_utils(axi_txn_filter) uvm_analysis_imp #(svt_axi_transaction, axi_txn_filter) inp; uvm_analysis_port #(svt_axi_transaction) write_ap; uvm_analysis_port #(svt_axi_transaction) read_ap; function new(string name = "axi_txn_filter", uvm_component parent = null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); inp = new("inp", this); write_ap = new("write_ap", this); read_ap = new("read_ap", this); endfunction virtual function void write(svt_axi_transaction tr); if (tr.xact_type == svt_axi_transaction::WRITE) write_ap.write(tr); else if (tr.xact_type == svt_axi_transaction::READ) read_ap.write(tr); endfunction endclass然后整个数据链就变成:
Port Monitor --> axi_txn_filter --> scoreboard.write_side --> scoreboard.read_side这种拆法还有个额外好处:统计带宽和延迟的时候,读写分开处理,不会乱。
5. 落地过程中一定会碰到的几个坑
5.1 重复采样和事务重叠
第一次接的时候,我老是发现Scoreboard里的事务数量比expected多了一倍。查到最后是VIP的monitor在多个level同时广播了transaction。有的版本默认系统级monitor和port monitor都开,同一条总线事务被广播了两次。解决办法是在配置里关掉低层级的monitor,或者在你的scoreboard里只接收某一个分析出口的数据。
判断方法很简单:在write_mon里打印tr的地址和ID,跑一个很短sequence,如果看到相同地址和ID的事务连续出现两次,就说明重复广播了。这时候别急着改scoreboard加去重逻辑,先回头检查VIP配置是不是重复使能了监控器。
5.2 valid/ready背压场景下的边界判断
第一次手写monitor的人,很容易把一次burst的边界算错。AXI协议里WLAST标志当前写burst的最后一拍,RLAST标志读burst的最后一拍,但这两个信号只有在VALID && READY同时拉高的周期才真正算数。如果你只是看到信号拉高就算完成,遇到长时间stall、插入wait states、甚至master中途重试,统计就会出现偏差。
Synopsys AXI VIP的Port Monitor内部已经处理了握手和背压逻辑,所以你通常不需要自己判断。但有一个例外:如果你在agent里包了一层“假monitor”,自己拿几个interface信号拼transaction,那就必须严格检查WVALID && WREADY之后才累计数据,WLAST && WVALID && WREADY才算burst结束。别小看这个逻辑,AXI协议的面试题里经常问valid/ready握手中谁等待谁的问题,工程里更是血泪教训。
5.3 打印太多卡死仿真
Port Monitor一旦跑起来,每个事务都会触发write。如果你在scoreboard里面顺手写了一个uvm_info打每一笔事务,瞬间日志会膨胀到几十GB,仿真速度掉一半,甚至把磁盘塞满导致仿真挂掉。我见过有同事在scoreboard里打了一行uvm_info(get_type_name(), tr.sprint(), UVM_MEDIUM),结果一个长回归跑完,日志文件2GB,打开都卡半天。
正确做法是:默认情况下只打印错误、或者只打印每100笔事务一次的摘要。需要调试的时候,再用verbosity或一个专门的print_control开关单独打开。我个人习惯是if (wr_cnt % 100 == 0)打印一次当前计数,既不卡仿真,又能确认数据还在跑。
6. 用Port Monitor的API做点更高级的事
6.1 延迟和带宽统计
事务对象里一般自带时间信息,svt_axi_transaction里常见的字段是start_time/end_time或者time_stamp。我在Scoreboard里统计延迟时,就是取tr.time_stamp之间的差值,再按事务类型分桶。
class delay_statistics; int wr_delay_sum; int wr_delay_cnt; int rd_delay_sum; int rd_delay_cnt; function void sample(svt_axi_transaction tr); if (tr.xact_type == svt_axi_transaction::WRITE) begin wr_delay_sum += tr.end_time - tr.start_time; wr_delay_cnt++; end else begin rd_delay_sum += tr.end_time - tr.start_time; rd_delay_cnt++; end endfunction function void report(); `uvm_info("DELAY", $sformatf("avg write latency = %0t", wr_delay_sum / wr_delay_cnt), UVM_MEDIUM) `uvm_info("DELAY", $sformatf("avg read latency = %0t", rd_delay_sum / rd_delay_cnt), UVM_MEDIUM) endfunction endclass带宽统计则用总数据字节数除以时间窗口。这个方法特别适合验证DDR控制器或者NoC,因为你可以直接看每一笔事务的地址分布,判断是否发生了热点访问或者大小段访问不均匀。
6.2 把异常事务自动抛给断言
Port Monitor收集到的事务不仅能喂Scoreboard,还能拿来驱动断言。比如你希望“每个slave的响应都必须是OKAY”,这也可以做成一个subscriber:
class axi_response_checker extends uvm_subscriber #(svt_axi_transaction); `uvm_component_utils(axi_response_checker) function new(string name = "axi_response_checker", uvm_component parent = null); super.new(name, parent); endfunction virtual function void write(svt_axi_transaction tr); if (tr.resp != svt_axi_transaction::OKAY) begin `uvm_error(get_type_name(), $sformatf("bad response %0s at addr %h", tr.resp.name(), tr.addr)) end endfunction endclass这里只是示意,实际resp枚举名要以你VIP头文件为准。思路就是利用同一个Port Monitor出口,把事务广播给多个消费者,而不用在scoreboard里把所有检查都堆在一起。等回归跑到一定阶段,你把各条检查点位打开,环境自己就能告诉你哪一笔事务、哪个地址、哪个响应出了问题。
如果你准备在下一个验证环境里接上这套东西,我最想提醒你的一件事是:不要等到所有代码都写完才开始接Port Monitor。先把最简单的一路“Port Monitor -> scoreboard -> 打印计数”跑通,确认数据能流出来,再去完善reference model和复杂比对。这就像写软件先打一个最小可行版本,验证环境的核心是反馈回路——数据能流、错误能报,剩下的优化都建立在“通路已经通了”这个前提之上。