news 2026/9/29 10:13:21

用Synopsys AXI VIP的Port Monitor快速连接Scoreboard:5分钟搭好UVM环境

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Synopsys AXI VIP的Port Monitor快速连接Scoreboard:5分钟搭好UVM环境

做AXI总线验证的同学应该都体会过这种痛苦:scoreboard想比对写通道的数据,得自己在monitor里拉着一堆信号,写一大堆协议解析代码,处理awvalid/awready握手、burst长度、wstrb掩码,一不留神还容易在乱序返回上翻车。后来我换用Synopsys AXI VIP之后,才发现这些工作其实VIP早就替你做完了——只要把它的Port Monitor用起来,一行connect就能把AXI各个通道的transaction干净利落地送进scoreboard。今天这篇就把这套连接流程完整拆开,从端口选型到代码实现再到排坑,全部基于我实际的工程经验。

这个方案适合正在搭UVM验证环境、想省掉自己写AHB/AXI monitor解析逻辑的验证工程师,也适合刚接触VIP、对TLM连接方式还不太熟的初学者。5分钟不是夸张,是真的够用,前提是你先把下面几个关键点看明白。

1. 先搞明白:Port Monitor在VIP里到底是个什么东西

1.1 它不是普通monitor,而是“总线嗅探器”

Synopsys AXI VIP在例化的时候,除了master和slave两个主动端之外,通常还会自动带出一个例化名为port_monitor_0的组件。这个组件在组件树里是独立存在的,不属于master agent,也不属于slave agent,它的角色是一个被动的观察者。

什么叫被动观察者?打个比方:master和slave是两个人来回传球,port_monitor就是坐在场边的统计员,它不参与传球,但每一个传球的动作都被它记录下来了。对应到总线上,它不驱动任何信号,不参与handshake仲裁,只是把自己“看到”的AXI通道transaction按协议格式整理好,然后通过analysis port广播出去。

很多朋友第一次用VIP时会忽略这个组件,因为直接在testbench顶层例化master/slave接口时,看起来port_monitor并没有连接任何总线信号。其实不用你手动接信号,VIP内部已经把它和总线监视逻辑绑定好了,它在仿真一开始就在默默工作,只是没人去“接收”它广播出来的数据而已。

1.2 它对外暴露了哪些analysis port

Synopsys AXI VIP的port_monitor把AXI协议按通道拆成了多个独立的analysis port,常见的是这么一组:

  • write_addr_channel_ap:写地址通道
  • write_data_channel_ap:写数据通道
  • write_resp_channel_ap:写响应通道
  • read_addr_channel_ap:读地址通道
  • read_data_channel_ap:读数据通道

每个ap上发射的transaction类型都是VIP自己定义好的AXI_TRANSACTION,里面包含了该通道完整的协议字段,比如id、addr、len、size、burst、data、resp等。具体字段名以你安装的VIP版本为准,但大体的结构是一致的:它已经把整个AR/ AW通道的协议细节都解析成了可读的属性,不用你再去盯着awvalid && awready做电平采样。

这里要特别说明为什么要强调“通道级”连接。AXI协议和AHB最大的不同就是通道分离且支持outstanding,写地址、写数据、写响应不是简单一一对应的关系,读数据和读地址之间还可能乱序返回。如果你的scoreboard想认真建模这种乱序行为,就必须从通道级去拿数据,而不是像AHB那样等一拍总线有效就能拿到完整报文。Port Monitor的通道级ap就是为这种场景准备的。

1.3 为什么你不再需要“手动抓信号”

以前我搭AHB/ AXI验证环境的时候,最烦的就是写monitor的采样逻辑。我自己写过一个AXI读数据通道monitor,要处理valid/ready握手、对齐、wrap回绕、last标记,还要自己拼数据数组,前后折腾了差不多两天才稳定。最后还要把transaction打给scoreboard,中间debug又花了一天。

换成port_monitor之后,这些全部被压缩成了一行代码:

env.axi_master_0.port_monitor_0.read_data_channel_ap.connect(sb.rd_data_fifo.analysis_export);

这行代码做完的事,等于我之前手动monitor里所有协议解析加TLM发送的工作量。不是说手写monitor没有价值,而是当强需求是快速搭建验证环境、验证DUT功能而不是验证总线协议本身时,直接用VIP现成的port_monitor是性价比最高的选择。

2. 连接之前,先想清楚TLM选型

2.1 analysis port的本质是“广播”

连接之前必须搞清楚一个核心概念:为什么port_monitor用的是analysis_port,而不是普通的uvm_blocking_put_port或者uvm_nonblocking_put_port。

这里有个特别直观的类比。普通的put/get端口是点对点的电话通话,你打给我,我就得接,还要等我说完才能挂。analysis端口更像是一个广播电台,port_monitor在电台上播报transaction,谁订阅了这个频道谁就能收到,收不到也不影响电台继续播。

这个特性对验证环境非常友好。同一个port_monitor的ap可以同时接scoreboard做数据比对、接coverage collector做功能覆盖率收集、再接一个reference model做预测,三个接收方各取所需,互不阻塞。而且analysis_port的write函数不会阻塞发送端,port_monitor该干嘛还干嘛,不会因为某个接收方处理得慢就把整条总线事务逻辑卡住。

还有一个隐藏的优点:因为analysis是“广播-订阅”模式,所以port_monitor这个被动组件注定不会反向影响VIP内部的时序。哪怕你把scoreboard整个删掉,对总线的仿真行为是零影响的。

2.2 uvm_tlm_analysis_fifo和uvm_analysis_imp,到底选谁

连接scoreboard的时候,最直接的问题是:我该用uvm_tlm_analysis_fifo还是自己写一个uvm_analysis_imp再加一个内部FIFO?

直接给结论:无脑优先选uvm_tlm_analysis_fifo。

原因很简单。uvm_analysis_imp需要你在scoreboard里实现一个write()函数,然后在函数里自己把transaction存入你自定义的队列或数组,所有存储和同步逻辑都是你自己的责任。而uvm_tlm_analysis_fifo是个现成的实现,它内部已经把FIFO行为和analysis_export封装好了,你可以直接把它的analysis_export拿出去connect,然后在scoreboard内部用get()、peek()等接口消费transaction。

两者的对比如下:

对比项uvm_tlm_analysis_fifo #(T)uvm_analysis_imp #(T, IMP) + 自建队列
对外接口自带analysis_export,直接connect需要自己实现write()
存储管理内部FIFO自动管理,支持阻塞/非阻塞自己建队列或uvm_tlm_fifo,管理逻辑自己写
使用复杂度低,声明+connect+get即用高,要写write()、要同步队列
适合场景绝大多数scoreboard接收端口需要write()内即时处理的特殊场景
常见坑几乎无忘了分配内存、write里用错队列

有人会问,那我能不能用uvm_tlm_fifo?能用,但要注意:uvm_tlm_fifo本身没有analysis_export,你没法直接把ap连上去。你得在scoreboard里定义一个uvm_analysis_imp,在write()里调用my_fifo.put(t),等于多绕了一圈。除非你有特殊的缓冲策略,否则这个绕路不值。

那什么时候用uvm_analysis_imp更合适?当你想在transaction进入FIFO之前就立刻做处理,比如根据类型做分流、过滤某些transaction、或者把数据同时复制到多个内部队列时,自己写write()反而更灵活。不过大多数场景下,我们只是想让scoreboard慢慢消费数据,uvm_tlm_analysis_fifo是最省心的选择。

3. 5分钟标准连接流程,照着抄就行

下面这套流程我实际跑过很多次,按步骤来基本能一次通过。

3.1 第一步:在scoreboard里声明并创建analysis_fifo

打开你的scoreboard类,在类声明区域加上你要接收的通道fifo。以接收写通道和读通道为例:

class axi_scoreboard extends uvm_scoreboard; `uvm_component_utils(axi_scoreboard) uvm_tlm_analysis_fifo #(AXI_TRANSACTION) wr_addr_fifo; uvm_tlm_analysis_fifo #(AXI_TRANSACTION) wr_data_fifo; uvm_tlm_analysis_fifo #(AXI_TRANSACTION) wr_resp_fifo; uvm_tlm_analysis_fifo #(AXI_TRANSACTION) rd_addr_fifo; uvm_tlm_analysis_fifo #(AXI_TRANSACTION) rd_data_fifo; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); wr_addr_fifo = new("wr_addr_fifo", this); wr_data_fifo = new("wr_data_fifo", this); wr_resp_fifo = new("wr_resp_fifo", this); rd_addr_fifo = new("rd_addr_fifo", this); rd_data_fifo = new("rd_data_fifo", this); endfunction endclass

注意一个特别容易被新手忽略的点:fifo必须要在build_phase里执行new()分配内存,不能在声明处直接初始化。UVM组件的new和内部对象的new是两回事,组件树的构建靠create_component,而fifo是普通对象,必须在build里手动创建,否则到了connect的时候访问的是一个null句柄,仿真直接报空指针。

3.2 第二步:在env或test里执行connect

connect的动作一般放在env的connect_phase里。这里假设你的环境里已经有axi_master_0这个VIP实例和一个scoreboard实例,实例名以你的环境为准。

function void connect_phase(uvm_phase phase); super.connect_phase(phase); // 写通道 env.axi_master_0.port_monitor_0.write_addr_channel_ap.connect( sb.wr_addr_fifo.analysis_export ); env.axi_master_0.port_monitor_0.write_data_channel_ap.connect( sb.wr_data_fifo.analysis_export ); env.axi_master_0.port_monitor_0.write_resp_channel_ap.connect( sb.wr_resp_fifo.analysis_export ); // 读通道 env.axi_master_0.port_monitor_0.read_addr_channel_ap.connect( sb.rd_addr_fifo.analysis_export ); env.axi_master_0.port_monitor_0.read_data_channel_ap.connect( sb.rd_data_fifo.analysis_export ); endfunction

这里有一个容易踩坑的点:不同版本的Synopsys AXI VIP,port_monitor的实例名和ap端口名可能不完全一样。有的版本里port_monitor实例名直接叫port_monitor,有的叫port_monitor_0。如果名字对不上,编译会直接报“找不到成员”之类的错,不要猜,用uvm_top.print_topology()或者直接查VIP的接口文档确定确切名字。

另外注意,如果是slave侧的port_monitor,你连的应该是slave实例下的ap,不要张冠李戴。master侧的monitor看到的是它发出去的事务,slave侧的monitor看到的是它收到的事务,两者语义不同。通常scoreboard做数据比对时,既会连master侧也会连slave侧,分别作为激励侧和响应侧的参考。

3.3 第三步:在run_phase里消费FIFO里的transaction

连上了却不在scoreboard里消费,等于白连。接下来在scoreboard的run_phase里写一个消费循环:

task run_phase(uvm_phase phase); AXI_TRANSACTION rd_addr_tr, rd_data_tr; forever begin // 阻塞等待数据进入fifo rd_addr_fifo.get(rd_addr_tr); rd_data_fifo.get(rd_data_tr); `uvm_info("SCB", $sformatf( "match addr=0x%0h data=0x%0h id=%0d", rd_addr_tr.addr, rd_data_tr.data, rd_addr_tr.id ), UVM_LOW) end endtask

get()默认是阻塞的,如果fifo里没有数据,task会挂在这里等待。这个特性让scoreboard的消费节奏天然地和总线上事务到达的节奏对齐:有数据就处理,没数据就休眠,不需要额外的手写同步逻辑。

如果你不想让scoreboard永久阻塞,比如想在仿真结束前做一次“检查fifo里是否还有积压数据”的操作,可以用try_get()配合used():

AXI_TRANSACTION tr; if (rd_addr_fifo.try_get(tr)) // 取到了数据 else // fifo为空

3.4 这5分钟到底是怎么分配的

很多朋友看到“5分钟”会觉得夸张,我说一下实际的时间分配,你感受一下:

步骤耗时关键动作
确认VIP里port_monitor和ap的名字30秒查文档或print_topology
在scoreboard里加fifo声明和build分配1-2分钟复制粘贴模板,改一下类型
在env里加connect语句1分钟5个ap对应的5行connect
写一个最简run_phase消费循环30秒get + 打印
编译跑一个用例验证收发1分钟看日志里有没有SCB打印

前提是你已经有一个能正常跑起来的VIP环境,不需要重新配VIP参数。如果是第一次搭VIP,那5分钟肯定不够,但熟悉之后,加一个新scoreboard的TLM连接路径,5分钟真的是绰绰有余。

4. 常见问题与排查实录,都是踩过的坑

4.1 明明connect了,为什么scoreboard一个transaction都收不到

遇到“fifo一直是空的”这种情况,先别慌,按顺序排查。

先确认connect是不是真的被执行了。UVM里connect_phase的调用顺序有讲究,但最常见的问题不是顺序,而是你connect的时候用错了实例。比如在test里this下面根本没有env这个句柄,或者env里没有scoreboard,connect语句虽然编译过了,但实际连的是一个悬空对象。

第二步,用uvm_top.print_topology()打印组件树,确认port_monitor_0的层级和你代码里写的完全一致。这一步能发现99%的“名字写错”问题。

第三步,在scoreboard的消费task里加一个探测打印,在get()之前每过一段时间打印一次rd_addr_fifo.used()。如果used一直为0,说明题出在发送端或连线;如果used在增长但你没消费出来,说明题出在消费逻辑。

还有一个容易被忽视的点:如果你在connect之前就把scoreboard里的fifo重新赋值为null,连接自然也就断了。有时候为了复位逻辑会不小心动到fifo句柄,这种运行时错误比较隐蔽,排查时可以加打印确认。

4.2 仿真日志被VIP的transaction打印刷屏,怎么关掉

这个问题从热词搜索结果里能看到,很多同行都在问。Synopsys AXI VIP默认的verbosity较高,尤其是当你开了transaction recording之后,每个transaction进出都会被打印,日志量大到爆炸,严重拖慢仿真速度,console窗口还刷得没法看。

有几种处理办法,从推荐到不推荐排列。

办法一,在test的build_phase里设置UVM config,把VIP内部的recording_detail降为0:

function void build_phase(uvm_phase phase); super.build_phase(phase); uvm_config_int::set(this, "*", "recording_detail", 0); endfunction

这种方式能有效减少VIP在transaction recording路径上的打印,通常是首选。

办法二,如果你的VIP版本自带打印开关,在VIP的config类里找到和transaction print相关的成员并置0。不同版本成员名不太一样,可能是enable_transaction_printing,也可能是print_transactions,建议直接搜一下VIP安装目录的uvm_macros或者参数定义,找到对应字段。

办法三,最暴力的方式是全局降低UVM verbosity,在仿真命令行加+UVM_VERBOSITY=NONE。这招能把所有UVM打印都关掉,但副作用是连你自己scoreboard里的uvm_info也不打了,一般不建议终极调试阶段用。

我个人遇到transaction刷屏时,习惯先打uvm_top.print_topology()确认链路,然后直接设置recording_detail为0。多数情况这一招就够了,日志从几GB直接降到几十MB。

4.3 uvm_tlm_fifo和uvm_tlm_analysis_fifo到底差在哪

既然热词里出现了这个问题,我就展开说清楚,因为它俩名字太像,实际用起来却有本质区别。

uvm_tlm_fifo #(T)是一个通用的TLM FIFO组件,它提供put()、get()、peek()这些标准的TLM端口方法。你想把一个analysis_port送来的数据存进去,必须自己写一个uvm_analysis_imp,在write()里手动调用fifo.put(t)。

uvm_tlm_analysis_fifo #(T)做的事更多:它对外暴露了一个analysis_export端口,内部把write()方法自动实现成了向内置FIFO存入数据。所以你可以直接把analysis_port的connect()方法接到它的analysis_export上,数据会自动进FIFO,完全不用自己写write函数。

一图胜千言:

使用方式uvm_tlm_fifo #(T)uvm_tlm_analysis_fifo #(T)
从ap直接connect不行,中间要加analysis_imp可以,直接connect analysis_export
需要自己写write()需要不需要
消费方式get()/peek()get()/peek()
设计意图通用的buffer,适合自定义数据通路analysis端口专属的buffer,适合scoreboard订阅

所以结论很简单:当你要对接的是port_monitor这种analysis_port时,直接选uvm_tlm_analysis_fifo,省掉一层自己写write()的功夫。只有当你要做两个组件之间自定义的点对点数据传递时,才需要考虑用uvm_tlm_fifo自己搭通路。

4.4 多通道乱序问题:不要“想当然”地按顺序匹配

前面提到AXI支持outstanding和多通道分离,这在scoreboard里是实实在在的坑,必须提前想好。

最直观的例子是读通道。测试里如果连续发多个读请求,读数据和读地址不是严格按顺序配对的,因为read data channel可以根据ID乱序返回。如果你的scoreboard简单粗暴地“先到先配对”,很可能把地址A的读数据当成地址B的返回,比对结果全错。

实际工程里,因为仿真时钟随机性、VIP内部建模的差异,这种乱序很容易复现。解决办法是建一个ID关联表,按id把读地址缓存起来,读数据到达时按ID查找对应的地址:

// 思路示例,不代表完整实现 AXI_TRANSACTION rd_addr_tr; AXI_TRANSACTION rd_data_tr; int id_queue[$]; // 收到读地址时,按id存入关联 rd_addr_fifo.get(rd_addr_tr); id_queue.push_back(rd_addr_tr.id); addr_map[rd_addr_tr.id] = rd_addr_tr.addr; // 收到读数据时,按id查地址 rd_data_fifo.get(rd_data_tr); if (addr_map.exists(rd_data_tr.id)) expected_addr = addr_map[rd_data_tr.id];

用关联数组按ID存期望地址,是一种很朴素的乱序处理思路。更复杂的场景(比如同一ID还有多个outstanding请求,需要按顺序排队)还要引入队列结构,但核心思想是一样的:不能用“到达顺序”来代替“ID匹配”。

写通道也有类似问题。写地址和写数据在协议上允许不按先后顺序到达,虽然大多数IP会先地址后数据,但严谨的scoreboard最好也做通道同步,不要假设它们严格交替。

4.5 一个容易被忽略的“连接方向”问题

最后提醒一点:analysis_port的connect方向。

ap.connect(export); // 正确 export.connect(ap); // 错误

这个错误我见过不止一次。connect()的方向必须是analysis_port调用,参数是analysis_export。写反了编译不一定报错,但运行到连接阶段会直接抛异常,而且报错信息有时候看不太懂。如果遇到诡异的“width mismatch”或者“port connection error”,先检查连接方向。

写在最后

说实话,用了Synopsys AXI VIP的port_monitor之后,我搭新scoreboard的速度比以前用手写monitor快了一倍不止,关键还省心。每次带新人搭环境,我都会先让他们把print_topology打印出来,看清楚port_monitor挂在哪个层级,然后把ap名字抄下来,剩下的就是一个一个connect的事。后续如果你想扩展功能,比如把同一份transaction同时送到coverage collector做覆盖率统计,直接在原来的ap上再连一条线就行,analysis广播天然支持多路订阅,完全不用动VIP内部逻辑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 10:11:03

【C++】多态——面向对象3大特性之一

为什么多态 继承:实现代码的复用多态:实现父子函数在调用(名字)相同的函数时实现不同的作用分类 编译时多态(静态多态):函数重载 和 函数模板运⾏时多态(动态多态):我们今天讲的多态…

作者头像 李华
网站建设 2026/9/29 10:10:20

计算机毕业设计 | SpringBoot+vue的图书馆管理系统(附源码)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 10:10:01

用Dify构建AI复盘助手:从事实提取到行动项生成的工作流实践

做项目管理第十年,我越来越觉得 Hindsight 这个词很妙。它的英文原意是"事后的理解",落到工作里就是我们常说的复盘、回顾、后见之明。你没法在项目进行时拥有它,但它往往比任何事前计划都值钱。为了把这种"事后视角"变得…

作者头像 李华
网站建设 2026/9/29 10:09:59

Altium Designer工程实战:约束驱动设计与可制造性闭环

1. 这不是“软件安装教程”,而是一份Altium Designer真实工程现场的生存指南Altium Designer,这五个字在PCB设计圈里,几乎等同于“吃饭喝水”一样的日常存在。但凡你做过哪怕一块四层板,跟Layout工程师开过一次评审会,…

作者头像 李华
网站建设 2026/9/29 10:09:01

ARM-Linux交叉编译工具链安装与Qt/Boost/chrony避坑指南

ARM-Linux 交叉编译工具链安装这件事,说简单也简单,apt 一条命令就能把 gcc-arm 拉下来;说麻烦也麻烦,真到 Qt、Boost、chrony 这些依赖上,工具链选错一个 ABI,后面全是坑。我这几年前后在 x86 笔记本、Ubu…

作者头像 李华