ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

用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 portSynopsys 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管理逻辑自己写使用复杂度低声明connectget即用高要写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里执行connectconnect的动作一般放在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 VIPport_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 addr0x%0h data0x%0h id%0d, rd_addr_tr.addr, rd_data_tr.data, rd_addr_tr.id ), UVM_LOW) end endtaskget()默认是阻塞的如果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里没有scoreboardconnect语句虽然编译过了但实际连的是一个悬空对象。第二步用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降为0function 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_VERBOSITYNONE。这招能把所有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内部逻辑。
返回列表