1. 先聊清楚:为什么我不再手动抓波形
做AXI相关验证的朋友,大概率都干过这件事:DUT跑飞了、数据对不上、或者时序崩了,第一反应就是打开波形,缩放、找信号、看握手,然后一边盯着AW、W、B、AR、R这几个通道的波形,一边在脑子里面自己翻译事务。这个过程三五分钟还好,一旦事务量上来,十几个outstanding请求混在一起,光是把一笔写请求和它对应的响应对上号,就能消耗掉大半天。我在前几年的一个项目里吃过不小的亏:一个数据一致性bug,定位了三周,最后发现是scoreboard自己在比对时漏掉了一个transaction。从那以后我就逐渐从“手抓波形”转向“让VIP把事务喂给scoreboard”,而Synopsys AXI VIP里的Port Monitor,正是帮我完成这一步的关键组件。
先说清楚Port Monitor是什么。它不是一个独立的第三方工具,而是Synopsys AXI VIP内部自带的事务级监视器。当你在验证环境中例化了一个AXI master VIP或者slave VIP,并开启对应的port monitor功能后,VIP会在端口层级帮你完成两件事:第一,识别AXI协议里的握手事件,把AW、W、B、AR、R各通道上的原始信号组合成有语义的transaction对象;第二,把这些transaction对象通过分析端口输出,供scoreboard、coverage collector或者其他参考模型消费。也就是说,你不必再写一堆task去采样信号、拼字段、等握手,VIP已经做好了,你要做的只是接一根“管道”,把数据导到该去的地方。
这篇文章适合谁来参考?第一类是刚接触UVM验证环境、想在项目里引入AXI VIP的验证工程师,第二类是已经被波形对比折磨得够呛、想把手动比对改成自动化检查的同学,第三类是已经有系统级验证经验、但对VIP内部机制还不够熟悉、想弄明白“为什么我连了ap却收不到transaction”的人。文章会围绕Port Monitor的机制、scoreboard的连接方式、关闭打印等实际高频问题展开,并给出一份可直接改使用的代码示例。每个环节我都会尽量讲清楚设计思路和踩坑点,而不是只贴一段能跑的东西。
2. 整体设计:Port Monitor在AXI验证环境里的位置
2.1 AXI VIP的内部结构,别只当一个黑盒子
用Synopsys AXI VIP之前,我建议大家先弄明白它大概分成哪几块。虽然不同版本、不同VIP包的层次结构有差异,但整体上会包含这么几个部分:接口(interface)、配置对象(configuration object)、驱动器(driver)、监视器(monitor)、事务对象(transaction)以及可选的协议检查器(protocol checker)和功能覆盖率模型(coverage model)。
通常我们会在测试平台里通过axi_vip_config这类配置对象来控制VIP的行为。比如地址宽度、数据宽度、ID宽度、协议版本(AXI3还是AXI4)、是否使能协议检查、是否使能覆盖率统计、是否开启port monitor,以及我们这篇文章里重点关注的“是否打印transaction”。这些配置项彼此独立,有些默认是打开的,有些是关闭的,而且在环境运行中不能随便修改,必须在build_phase里提前设好。
很多工程师喜欢把VIP当黑盒子,连完接口就撒手不管。这个做法不能说错,但一旦遇到“收不到数据”或者“协议误报”之类的问题,你不了解内部结构,排查起来会非常被动。我自己的习惯是:拿到VIP先看它的用户手册里关于“monitor architecture”这一节,弄清monitor挂在哪个端口、分析端口叫什么名字、事务在什么时候从monitor发射出来。知道这些,后面连scoreboard的时候就能少踩一半的坑。
2.2 Port Monitor的数据流:握手信号变成事务对象
Port Monitor在整个环境里的位置,可以这样理解:它贴着VIP的物理端口(也就是连接DUT的那一侧)工作。DUT和VIP之间在跑真实的AXI时序,有READY、VALID、LAST这些信号在跳变。Port Monitor要做的事情,是把这一堆底层信号按协议规范“翻译”成你我在验证层面更关心的东西——一笔读请求的地址是多少、突发长度是多少、每拍数据是什么、对应的ID是什么、响应状态是OKAY还是SLVERR。
举一个写操作例子。当AW通道上的握手完成,Port Monitor会生成一个写地址事务;当W通道上一拍一拍的数据都传完,它会生成写数据事务;当B通道响应回来,它再把地址、数据、响应组合成一个完整的写操作事务。具体是分开发射还是合并发射,取决于VIP的实现和配置。读操作类似,AR通道握手上来之后,R通道的所有数据拍都收集齐了,才会生成一个完整的读事务。
这里要注意一个我非常看重的细节:Port Monitor采样事务的依据是握手成功,而不是信号本身变化。也就是说,如果AWVALID拉了但AWREADY没有拉起来,这个请求还处于等待状态,Port Monitor不会提前把它当成一笔完成的事务发出去。这个特性和手抓波形时容易出现的“把pending请求也算进去”的错误形成了鲜明对比,自动化采集的准确性也体现于此。
2.3 为什么用Port Monitor而不是自己写monitor或看波形
有人会问:既然VIP内部已经有monitor了,为什么有些同事还要自己写一个bus monitor?答案通常有二,一是他们没注意到VIP自带输出口,二是他们担心VIP的monitor和自己环境里的数据格式不匹配。但实际上,自己写monitor的成本远远被低估了。一个能处理AXI握手、outstanding乱序、突发传输、窄突发、ID重映射的bus monitor,写起来轻松上千行,而且边界情况你很难一次覆盖全。写完后还得不断维护、修bug,这个时间成本足够你做很多事情了。
至于波形对比,更大的问题是效率太低。波形能帮你在问题已经发生之后回溯现场,但它不擅长在第一时间告诉你“哪一笔出了问题”。尤其是AXI这种高度流水化、支持多outstanding的协议,同一时刻总线上可能同时有几十笔事务在发送或者返回,波形上看过去满屏都是VALID和READY的交叉脉冲,你根本没办法肉眼跟踪每一笔的因果关系。Port Monitor则不同,它把信息从“信号级”抬升到了“事务级”,输出的是结构化对象,直接可以拿去和参考模型对比,整个过程是自动化的、可重复的、可扩展的,也是可以跑回归的。
3. 核心细节解析:Transaction长什么样,怎么拿,怎么用
3.1 事务字段:scoreboard里要用到的关键信息
拿到AXI事务之后,首先得知道它里面有什么。Synopsys AXI VIP的transaction类,字段命名在不同版本里可能略有差异,但协议层面对应的内容基本一致。以一个典型的写事务为例,你会看到几组信息:
地址相关字段,包括addr、burst_type(FIXED/INCR/WRAP)、burst_len(实际拍数减一)、burst_size(每拍字节数),这些决定了一次突发访问的地址范围。数据相关字段主要是写数据队列和对应的字节使能,比如data[]、wdata[]和wstrb[],在事务对象里通常会按拍存放。ID和响应用来追踪outstanding请求和返回状态,比如id、resp。除此之外,事务里还经常带时间戳信息,标记采样到事务的时刻,这在性能分析和延迟测量里很有用。
读事务的结构类似,只是把写数据队列换成了读数据队列。当你拿到一个读事务,字段里会有从DUT返回的数据和对齐信息。
拿到底层字段之后,scoreboard就可以做两类检查:一是单向检查,比如读响应里的数据是否和预期一致,或者响应类型是否与地址区间匹配;二是因果检查,比如发出去的写请求,最后是否收到了响应,响应的ID是否和请求一致,数据顺序是否满足协议要求。所以在写scoreboard之前,把事务对象的所有字段列一张表,标好哪些是比对需要的关键字段,哪些是辅助判断字段,这个习惯能让你后面省很多麻烦。
3.2 分析端口:怎么把事务从VIP里“接”出来
在UVM环境里,组件之间传递transaction最标准的方式就是TLM analysis port和analysis fifo。Synopsys AXI VIP的monitor一侧通常会提供analysis port,也就是我们常说的ap。你要做的就是在scoreboard里准备好对应的TLM接收端,然后用一句connect把两边接起来。
比较常见的接法有两种。如果scoreboard和monitor在同一层环境里,比如都在一个axi_env下面,那直接在connect_phase里写agent.monitor.ap.connect(scb.analysis_export);就可以了。如果scoreboard在更上层,你还需要在axi_env上再开一个analysis port,把monitor的数据继续向上转发。
这里有一个很容易被忽略的细节:TLM的connect是单向的点对点连接,不能一个source端口同时连两个consumer就完事了——严格来说可以多连,但你要考虑数据是被广播给所有下游。若你既想送给scoreboard,又想送给functional coverage collector,那么两个都要connect到ap上,它们都会收到同一份transaction拷贝。这个行为在TLM里是合法的,但我建议在代码里写清楚注释,避免后接手的人以为connect错了。
我个人更喜欢在scoreboard内部放一个uvm_tlm_analysis_fifo,而不是自己写一个analysis imp然后手动管理队列。原因很简单:analysis fifo本身是线程安全的,自带put和get两侧接口,数据先进先出,天然满足事务到达的先来后到;而且它内部已经处理好了TLM接口转发,你只需要重写write函数,或者直接在scoreboard的主体任务里消费队列即可。
3.3 采样时机和同步:别在复位和时钟边界上翻车
Port Monitor的输出看起来是“自动的”,但它的采样时机还是有一些讲究。第一是时钟域问题:AXI VIP会跟着接口时钟工作,如果你的DUT有多个时钟域,比如读通道和写通道来自不同驱动逻辑,那么VIP通常会按照协议要求同步所有通道到参考时钟。事务发射的时刻,并不等同于物理信号采样的时刻,通常会有一定的延迟。
第二是复位问题。仿真开始后如果在复位还没有释放的时候,DUT先出现了部分信号变化,Port Monitor可能丢掉这些不完整的事务,或者抛出协议错误。解决思路是:在测试序列里保证复位释放并且稳定几个时钟周期之后,再发起真正的AXI读写操作。这个时序关系虽然看起来像测试序列的职责,但在排错时你要能想到它,不然你对着VIP的协议检查报告会一头雾水。
第三是多outstanding的完成顺序。AXI协议允许乱序返回,也就是说你发了读请求A、B、C,返回的顺序可能是C、B、A。Port Monitor不会帮你排序,它只会忠实反映物理总线上事务被观察到的顺序。因此,scoreboard在做读数据比对时,一定不要简单地把期望队列按照发送顺序pop来匹配,而应该用事务ID或地址把请求和响应关联起来。这一点我在第5节会展开讲,这里先埋个伏笔。
3.4 关闭transaction打印:控制台清静的实用技巧
很多用Synopsys AXI VIP的工程师都会遇到一个现象:测试跑起来之后,终端窗口里刷出一堆transaction打印,长到连log文件都几十MB起步。这个问题几乎成了AXI VIP使用中的高频痛点,网上搜“Synopsys AXI VIP如何关闭transaction打印”这个词条就很能说明问题。其实解决方案不复杂,关键是要找对配置项。
最常规的做法是在VIP的configuration对象里找到类似print_transactions、enable_msg_log或者transaction_logging这样的开关,把它设为0。不同版本的VIP命名有差异,但思路一致。比如:
function void build_phase(uvm_phase phase); axi_vip_cfg = axi_vip_config::type_id::create("axi_vip_cfg"); axi_vip_cfg.print_transactions = 0; // 关键:关掉事务打印 // 其他配置... endfunction如果你的VIP支持通过层次路径单独控制某一端口,也可以使用UVM的config db在顶层强制覆盖:
uvm_config_int::set(this, "*.env.axi_mst.*", "print_transactions", 0);需要注意,关闭打印之后并不会关闭Port Monitor本身,事务依然会通过analysis port送出来,scoreboard照常运行。这个开关只是控制uvm_info打印。
另外提一个小技巧:调试阶段如果还想看部分关键事务,不要全开全关,可以给不同端口设置不同打印级别,或者用uvm_info的可重载verbosity来控制。这样既不刷屏,又能在需要的时候保留现场。
3.5 Port Monitor的开关:什么时候开,什么时候不开
虽然我们整个项目的核心是“用Port Monitor自动收集事务数据”,但并不是说在任何验证场景里都必须把它打开。Port Monitor会在接口上做事务识别,这本身会占用一定的仿真资源。如果你的环境里只是做简单的AXI从设备功能测试,或者数据量极大、仿真时间紧张,可以考虑只对少数必要的VIP端口开启port monitor。
还有一种情况是,你例化了AXI VIP,但并没有使用它的master驱动能力,只是希望它扮演一个被动监听者。这种情况通常应该把VIP配置成active/passive模式中的passive,此时驱动器和定序器不工作,只有monitor和协议检查器在运行。Port Monitor依然可以开启,它会把总线上所有事务都采集下来。我见过有些项目为了省事,把一个active master VIP挂在已经由其他agent驱动的总线上,结果驱动冲突、协议检查一路飘红。正确做法就是优先配置passive模式。
4. 完整实操:连接Scoreboard的代码与配置
4.1 配置VIP:把Port Monitor打开
我做这类环境时,习惯先把配置环节单独拎出来,写清楚每一步的作用。下面是一段基于常见Synopsys AXI VIP接口风格的配置示例,你可以按自己实际使用的VIP版本调整类名和字段名:
class axi_env_cfg extends uvm_object; `uvm_object_utils(axi_env_cfg) rand bit enable_port_monitor; rand bit print_transactions; rand int data_width; rand int addr_width; rand int id_width; constraint c_default { enable_port_monitor == 1; // 核心开关:必须打开 print_transactions == 0; // 默认关闭事务打印 data_width == 64; addr_width == 32; id_width == 4; } function new(string name = "axi_env_cfg"); super.new(name); endfunction endclass在测试用例中创建并设置这个config对象,然后通过config db传给VIP:
class axi_base_test extends uvm_test; `uvm_component_utils(axi_base_test) axi_env_cfg env_cfg; axi_env env; function void build_phase(uvm_phase phase); super.build_phase(phase); env_cfg = axi_env_cfg::type_id::create("env_cfg"); env_cfg.enable_port_monitor = 1; env_cfg.print_transactions = 0; uvm_config_object::set(this, "env", "cfg", env_cfg); env = axi_env::type_id::create("env", this); endfunction endclass如果VIP的monitor本身还有独立的analysis port开关,也建议显式开启。比如某些VIP会提供一个类似set_monitor_analysis_port_enable(1)的接口。总之,端口监控的“数据出口”要打开,否则后面connect得再紧密,也收不到内容。
4.2 环境层级:如何把monitor的ap和scoreboard接起来
下面给一个简化的axi_env,里面例化了master agent、slave agent和scoreboard三个主要组件。master agent里的monitor会通过analysis port向scoreboard发送它观察到的事务, slave agent同理。我们在连接阶段把两条数据流分别接到scoreboard的不同fifo上。
class axi_env extends uvm_env; `uvm_component_utils(axi_env) axi_master_agent mst_agent; axi_slave_agent slv_agent; axi_scoreboard scb; axi_env_cfg cfg; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_object::get(this, "", "cfg", cfg)) `uvm_fatal("AXI_ENV", "failed to get cfg") mst_agent = axi_master_agent::type_id::create("mst_agent", this); slv_agent = axi_slave_agent::type_id::create("slv_agent", this); scb = axi_scoreboard::type_id::create("scb", this); endfunction function void connect_phase(uvm_phase phase); super.connect_phase(phase); // 关键连接:master monitor 和 slave monitor 分别接到scoreboard mst_agent.monitor.ap.connect(scb.mst_fifo.analysis_export); slv_agent.monitor.ap.connect(scb.slv_fifo.analysis_export); endfunction endclass这里两个fifo的类型都是uvm_tlm_analysis_fifo #(axi_transaction)。master monitor里的transaction代表VIP作为master主动发起的事务,slave monitor里的transaction代表VIP模拟从设备时观察到的事务。两者可以分别用于发起侧和响应侧的独立观测,也可以用于更复杂的协议一致性检查。
4.3 Scoreboard内部实现:接收事务、解析字段、做比对
scoreboard的设计取决于你的验证目标。最简单的一类scoreboard是“预期模型对比”,即用一个参考模型产生期望数据,然后拿VIP monitor送来的真实事务做比较。复杂一点的,可能是带延迟模型的乱序比对和时序统计。下面给出一版典型的结构,它接收master和slave两侧的事务,并把写数据、读数据和参考值做简单比对。
class axi_scoreboard extends uvm_scoreboard; `uvm_component_utils(axi_scoreboard) uvm_tlm_analysis_fifo #(axi_transaction) mst_fifo; uvm_tlm_analysis_fifo #(axi_transaction) slv_fifo; task run_phase(uvm_phase phase); fork process_write_side(); process_read_side(); join endtask protected virtual task process_write_side(); axi_transaction tr; forever begin slv_fifo.get(tr); if (tr.kind == axi_transaction::WRITE) begin check_write_data(tr); end end endtask protected virtual task process_read_side(); axi_transaction tr; forever begin slv_fifo.get(tr); if (tr.kind == axi_transaction::READ) begin check_read_data(tr); end end endtask protected virtual function void check_write_data(axi_transaction tr); // 在这里和参考模型对比 foreach (tr.data[i]) begin expected[i] = predict_data(tr.addr, i); if (tr.data[i] !== expected[i]) `uvm_error("SCB_WR", $sformatf("addr=%0h beat=%0d exp=%0h act=%0h", tr.addr, i, expected[i], tr.data[i])) end endfunction protected virtual function void check_read_data(axi_transaction tr); // 读数据比对逻辑,同样要处理ID和地址映射 endfunction function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); mst_fifo = new("mst_fifo", this); slv_fifo = new("slv_fifo", this); endfunction endclass这段代码的逻辑并不复杂,但你可别小看“从fifo里拿到transaction”这一步。真实项目里,scoreboard里经常同时挂着几十个端口的事务流,有AXI的、有APB的、有AXI-Lite的,甚至还有软件运行端发来的配置命令。如果你不把数据流按端口或者事务方向拆开,run_phase里会乱成一锅粥。所以我在项目里习惯为每一路数据流单独建一个fifo、单独建一个处理task,宁可多写几个task,也不要在一个task里靠判断条件处理多路数据。
4.4 参考模型怎么和数据流同步
只要做数据比对,就避不开“参考模型在什么时候产生期望值”这个问题。常见做法有两种:第一种,参考模型在事务发起时就提前算出期望结果,放进一个参考队列;第二种,参考模型在收到事务后再实时计算。
第一种做法的好处是贴近实际系统行为,尤其在验证缓存一致性或者流水线处理器时,DUT的最终输出往往由历史请求序列共同决定,你必须在请求发出时建立“期望状态”。它的风险在于,当outstanding乱序时,期望队列的pop顺序未必等于事务返回顺序。稳妥的做法是用ID或者地址做一个map,而不是简单维护一个FIFO队列。
第二种做法适合纯组合逻辑类模块或者协议转换桥,比如AXI到APB的桥,输入事务到桥,输出就是确定性的APB写事务,可以直接比较。这种情况下不需要提前存期望队列,收到事务后实时算就行。
不管用哪种方式,我强烈建议在scoreboard里保留两种模式:严格模式和参考模式。严格模式要求事务顺序完全一致,适用于不带乱序的开发初期;参考模式允许乱序,用ID或事务标识匹配,适用于流水线稳定的中后期。我的经验是,不要一上来就启用参考模式,否则环境里隐藏的顺序bug会被乱序逻辑掩盖,后面定位起来更痛苦。
4.5 测试序列:让数据真正跑起来
完成连接之后,测试序列还是老一套,但你可以利用VIP提供的sequence随机生成AXI流量。比如发一个带多个outstanding的读写混合序列,让Port Monitor在总线上采集大量事务,再自动送到scoreboard。序列代码大致是这样:
class axi_rw_test extends axi_base_test; `uvm_component_utils(axi_rw_test) function new(string name, uvm_component parent); super.new(name, parent); endfunction task run_phase(uvm_phase phase); axi_master_rw_seq seq = axi_master_rw_seq::type_id::create("seq"); phase.raise_objection(this); repeat (20) begin if (!seq.randomize() with { addr inside {[32'h0000_0000 : 32'h0000_FFFF]}; }) `uvm_fatal("TEST", "randomize failed") seq.start(env.mst_agent.sequencer); end phase.drop_objection(this); endtask endclass跑完这个测试,你会看到scoreboard的日志里出现若干SCB_WR或者SCB_RD相关消息。如果一切正常,几乎没有输出,终端一片安静;只有当数据不一致时,它才会弹出一行带地址、拍数、期望值和实际值的错误报告。这就是自动化验证和手抓波形最大的区别——手动靠眼睛找错,自动化靠代码报错。
5. 常见问题与排查技巧实录
5.1 为什么我明明connect了,scoreboard却收不到transaction
这是我在社区和项目群里被问得最多的问题。连了ap.connect,但scoreboard的fifo一只空着,什么事务都收不到。归纳下来,原因不外乎这几种。
第一,Port Monitor的开关没打开。有些VIP默认不开启port monitor,或者只在passive模式下才开启,如果你的配置把monitor关了,那么ap端口上自然什么都没有。排查方法很简单:在scoreboard的write函数里加一个uvm_info打印,或者在VIP配置里临时打开print_transactions,看看终端有没有事务输出。
第二,时钟没跑或者复位没释放。monitor要跟在接口时钟下采样,如果时钟一直处于停顿状态,它自然采集不到任何握手。复位长期拉低也会让VIP内部状态机卡死。这种情况看起来像是VIP坏了,实际是你的DUT没有起来。
第三,连接顺序错了。TLM连接在connect_phase执行,但如果你在build_phase里就把analysis_export拿来当普通端口使用,那只会拿到空的代理对象。排查方式是把connect语句打上层次打印,确认mst_agent.monitor.ap存在、类型正确,再确认scb.mst_fifo.analysis_export存在。
第四,事务被其他组件“先消费”了。如果你的scoreboard里既有analysis fifo又额外挂了analysis imp,且两个模块都连接到同一个ap上,那么每个模块都会收到同一份transaction。这不算问题,但你得确保消费方都做了正确的处理,没有哪一个还在用传统的事件队列去等数据。还有一次,我遇到一个项目里有人在scoreboard的build_phase中把fifo误创建成了local对象,连接时拿到的还是空指针,最后编译没错但运行时直接crash。所以连接之后,第一步务必检查fifo是否为空的引用。
5.2 transaction打印刷屏,仿真速度被拖垮
遇到这个问题的朋友不少。事务打印过多,不只是难看,它还会显著拖慢仿真,因为每次事务发射都会触发字符串格式化,打印到终端又会等待文件I/O。数据量一大,整个测试跑完的时间可能从十分钟变成一小时。
我个人的处理顺序是这样的:先找到VIP配置对象里的打印开关,关掉它;如果关不掉,再尝试用UVM的uvm_config_int按路径覆盖;最后实在不行,才考虑在源码层面拦截打印。在多数Synopsys AXI VIP版本里,关闭打印的配置项是显式提供的,并不会出现关闭不了的情况。真正麻烦的是,有时你会把print_transactions设成0,但VIP内部还有另一套基于uvm_info的详细打印,标题类似“AXI transaction begin”或“AXI transaction end”,这类打印需要你通过设置UVM_VERBOSITY或uvm_report_verbosity来屏蔽。
我的经验是:不是所有打印都要一刀切。调试master agent和slave agent之间的交互时,我常常只打开master agent的transaction打印,关闭slave agent的;等确认握手没问题,再把master agent的也关掉。这样既不会在调试初期显得毫无头绪,也不会在回归阶段被日志淹没。
5.3 事务收到后字段对不上,scoreboard误报
有时候scoreboard会频繁报错,但打开波形一看,总线上数据明明是对的。这种“误报”多数源自字段映射理解偏差。AXI协议里有两个极其容易混淆的字段,一个是burst_len,一个是burst_size。
burst_len在AXI协议里是实际拍数减一。比如一笔8拍的INCR突发,burst_len字段的值是7。很多人第一次写scoreboard时直接拿burst_len当拍数用,结果期望数据数量永远比实际少一拍,自然报错。另一个是burst_size,它表示每拍传输的字节数,是2的幂次对数,比如burst_size等于3表示8字节。如果你把burst_size当成实际字节数用,算出来的地址范围和期望值全是错的。
还有一个细节是写数据的字节使能wstrb。在窄突发或者非对齐访问中,某些字节通道会被屏蔽,DUT实际写入存储器的数据可能和scoreboard里显式给出的wdata并不完全一致。比对时如果忽略wstrb,会误判数据错误。正确的做法是:按wstrb把期望数据也做掩码,只比较被使能的字节。
5.4 多outstanding乱序,scoreboard比对顺序错乱
AXI协议允许不同ID的事务乱序完成,同一ID内需要保持顺序。如果你在scoreboard里只用了一个先进先出的期望队列,且队列的入队顺序是请求发出顺序,那么当返回顺序和请求顺序不一致时,比对就会产生大量假错误。
我的解决办法是引入基于ID的期望缓冲。具体做法是:在请求侧把期望值按ID分组,存在一个关联数组或queue of queue中;在响应侧,每收到一个事务就根据它的id字段去对应的队列里取出最早的期望值,然后做比较。这样即使不同ID之间乱序返回,scoreboard也完全能跟上。
代码示意如下:
class axi_scoreboard extends uvm_scoreboard; // ID -> 期望队列 protected axi_transaction exp_q[$]; protected axi_transaction exp_by_id[bit [3:0]]; function void push_expected(axi_transaction tr); exp_by_id[tr.id].push_back(tr); endfunction function void check_read_data(axi_transaction tr); axi_transaction exp; if (exp_by_id.exists(tr.id) && exp_by_id[tr.id].size() > 0) begin exp = exp_by_id[tr.id].pop_front(); compare(exp, tr); end else begin `uvm_error("SCB_RD", $sformatf("unexpected read id=%0d", tr.id)) end endfunction endclass当ID宽度较大时,建议用队列而不是关联数组?其实关联数组完全够用,只是队列数量较多时需要额外管理内存清理。如果你不想为每个ID单独建队列,也可以用一个统一的expect_queue按“ID + 地址”做匹配查找。不过这种方法在同一个ID持续大量传输时效率会下降,建议按项目实际流量规模来选型。
5.5 时钟复位和VIP内部时序带来的延迟
最后说一个容易被表象误导的坑。Port Monitor发射事务的时刻,和DUT端口上实际完成事务的时刻之间,可能存在几个时钟周期的延迟。如果你在scoreboard里做时序断言,比如测量读延迟,一定要读事务对象里的时间戳字段,而不是用scoreboard收到事务的仿真时间。
我见过有人拿$time去测延迟,结果每次都比预期大好几个周期,最后才发现VIP在内部做了缓冲。纠正方式是使用VIP事务对象自带的cv_start、cv_end或者类似的timestamp字段。如果没有这些字段,就需要在测试序列里记录事务发起时间,并与scoreboard收到的时间做对照。用$time做精确延迟统计,在带有VIP buffer的情况下,几乎必然踩坑。
6. 写在最后的一点经验
如果你正准备把项目里手抓波形的习惯切到Port Monitor这条路上,我建议不要一上来就在成熟环境里大改。先用一个小模块,搭一个最小验证环境,把master VIP挂上去,开Port Monitor,连一个最简单的scoreboard,跑通一笔写数据,再逐步扩展。这个小环境会帮你快速理解VIP的配置接口和事务对象,也能让你后面的排错有一个干净、可控的基线。我当年就是靠着几十行代码的小环境,把AXI VIP的monitor行为摸了个七八分,等到集成到真实DUT环境时,极少再被“为什么收不到数据”这类问题卡住。自动化采集事务这件事,本质上不是让验证框架变得多复杂,而是把重复劳动交给工具,让人的精力花在真正需要判断力和调试能力的地方。