ARTICLE DETAIL

资讯详情

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

UVM复位后sequence停不干净?stop_sequences()用法与避坑指南

UVM复位后sequence停不干净?stop_sequences()用法与避坑指南 最近在项目里调一个用例碰到了非常典型的“复位后环境污染”问题模块第一次上电跑配置流程完全正常一旦在运行中拉低复位、再释放复位第二段sequence就像死了一样一个transaction都发不出去scoreboard那边瞬间积压了大量超时和事务丢失。查了两天问题根子不在DUT而在验证环境自身——复位之后上一轮sequence还挂在sequencer上没走干净。后来把stop_sequences()的调用时机和sequence的退出机制理顺问题才算彻底解决。其实这种问题在UVM验证环境里非常普遍尤其做模块级或子系统级验证时复位不是只在仿真最开始来一次更多是反复出现做电源测试、时钟切换、软复位异常处理、寄存器读写中断恢复都会用到复位。这时候如果环境里的sequence管理不好轻则用例跑飞重则整个回归都是随机失败。这篇就围绕stop_sequences()这个接口把环境复位的处理思路、代码模板、以及我实际踩坑的排查过程完整拆一遍希望能给正在被复位sequence坑到的朋友一些参考。1. 复位环境下sequence“刹不住车”的典型现场1.1 三种常见的复位触发方式与验证环境的真实状态Helloworld级别的testbench把复位当成一种“初始条件”但在真实项目中复位是贯穿整个仿真过程的动态事件。我归纳了一下主要有三类上电复位仿真开始时拉低复位时钟稳定后释放这种一般厚道一些给了timeout等待环境初始化完成。运行中软复位DUT内部寄存器写入复位命令或外部管脚拉低此时总线上可能还有未完成的读写事务。这是最容易出问题的场景。局部复位比如只复位某个IP子模块或某个时钟域其他模块还在工作这种情况下验证环境的“停止范围”必须精确否则会把还在正常工作的模块也误伤。无论哪种复位验证环境并不是“回到时间零点”它仍然以UVM对象的形态存在sequencer里面有排队的sequence、driver还握着一个半途的transaction、register model的mirror值停留在复位前状态、scoreboard的reference model还有积压数据。所有这些东西不会因为DUT复位信号拉低而自动清零。1.2 不处理sequence终止导致的四个典型故障我见过太多次这类故障症状各不相同但都有一个共同特征复位之后环境行为异常且每次失败的用例都不一样。典型的有四类。第一是sequence残留。上一轮的SPI配置sequence还没跑完复位来了它挂在wait_for_grant或者get_response上不退出。复位释放后新sequence也往同一个sequencer上start两个sequence共用一个driver发出去的事务混杂在一起总线协议直接乱掉。第二是phase超时。UVM的phase机制有objection机制如果sequence的body没退出它如果raise了objection当前phase永远drop不了最终仿真被timeout卡死。很多团队遇到phase timeout第一反应是查objection泄漏但根因其实是复位没把sequence停干净。第三是scoreboard误报。复位后reference model没有跟着DUT一起复位导致之后收到的transaction全部对不上本应通过的环境出现大量比对错误。第四是个容易被忽略的现象sequence明明没有收到response却只能发出固定数量的包比如网上常有人问“uvm不回respond但也只能发八个包”。这其实往往是sequencer内部的事务队列没有复位旧的request还在仲裁队列里占着位置新sequence的request进不去或者driver一直不回复sequence积压到一定数目后启动新item的能力被耗尽。2. stop_sequences()的设计意图不是暴力停止而是协商停机2.1 从sequence与sequencer的握手关系看停止语义要搞清楚stop_sequences()为什么不是万能的得先理解UVM里sequence和sequencer之间是怎么协作的。sequence产生transaction但并不会直接把数据给driver而是通过start_item()和finish_item()两步完成一次“申请-仲裁-授权”的握手。start_item()内部调用了wait_for_grant()相当于sequence向sequencer提交了一个请求说“我想发送一个item”。sequencer根据仲裁算法默认是FIFO也可以是优先级、locked sequence等选出当前winner给sequence发授权。sequence拿到授权后开始随机化item然后finish_item()内部调用send_request()把item交给driver同时wait_for_item_done()等待driver处理完。这个机制是协作式的不是抢占式的。stop_sequences()并不是去强行终止某个进程而是把sequencer这端的“调度闸门”关上已经排队的sequence请求面对的不再是一个正常仲裁的sequencer而是一个不再授权的sequencer。这样还没有拿到grant的sequence就会在wait_for_grant处被卡住或失败正在body中运行的sequence如果再次进入握手流程也会感知到停止状态并退出。你可以把这个过程类比成一个外卖派单系统stop_sequences()不是把骑手手里的订单直接抢走而是不再给骑手派新单。已经取了餐的骑手还能跑完手上的订单但再也没有新活了。2.2 stop_sequences()不是杀线程已启动的sequence并不会被强制打断这里必须强调一个很多新手会误读的点stop_sequences()不会去fork_kill你sequence里已有的线程。它的作用是让sequencer不再grant新的sequence请求但如果有sequence正阻塞在一个不经过sequencer仲裁的等待点上比如uvm_event.wait_trigger()、get_response()、或者一个大delay语句里那么它不会被stop_sequences()触发仍然继续挂着。这也是“我都调了stop_sequences()为什么sequence还在跑”的最常见原因。真正的含义是调用stop_sequences()之后当前正在运行的sequence会在下一个“与sequencer通信”的边界处感知到停止并退出但如果它一直不通信那它就一直不会退出。所以在复位场景中stop_sequences()只是一部分工作还有另一部分工作必须由sequence自己配合完成——要么在body里监听复位事件要么在关键事务边界检查复位标志。只靠一句stop_sequences()包打天下一定会踩坑。2.3 stop_sequences()、kill_sequences()与stop()的边界UVM里还有几个容易和stop_sequences()混淆的接口我在代码评审时经常见到有人用错这里做一个简单对比方法作用对象停止强度适用场景stop_sequences()sequencer上的所有sequence调度停止grant新sequence无法继续获得授权已启动sequence在下一个仲裁点退出环境复位、需要暂停激励但不破坏sequencer实例kill_sequences()sequencer上的所有sequence强制终止直接清理仲裁队列和相关句柄已经确认sequence卡死、且不需要它优雅退出的场景sequence.stop()单个sequence对象将单个sequence置为停止状态通知其退出精确控制某一个sequence不影响其他sequenceuvm_sequence::kill()当前sequence自身终止该sequence及其子sequence在sequence内部发现异常时自杀式退出绝大多数复位场景推荐用stop_sequences()因为它语义温和给sequence留了清理的余地。如果复位信号持续时间很短但环境里的sequence已经明显卡死比如get_response永远等不回来那用kill_sequences()更直接。但注意kill_sequences()对正在运行的sequence来说可能伴随“死亡通知”如果sequence里有fork的线程没有回收容易造成资源泄漏要谨慎。3. 一套干净的复位控制流程从reset_agent到sequence的完整链路3.1 reset_agent中的事件广播设计处理环境复位首先要让整个环境都能感知到复位信号而不是只在DUT侧拉一下管脚。我习惯在reset_agent里用一个uvm_event来广播复位事件这样driver、sequencer、sequence、scoreboard都能按需监听。reset_agent本质上是一个简单的驱动器它通过virtual interface驱动复位信号。但除了驱动信号它还会在复位拉低和释放释放时各触发一次reset_event。代码大概长这样class reset_agent extends uvm_agent; uvm_component_utils(reset_agent) reset_driver rdriver; uvm_event reset_event; uvm_phase current_phase; function new(string name, uvm_component parent); super.new(name, parent); reset_event new(reset_event); endfunction virtual task run_phase(uvm_phase phase); forever begin (negedge vif.resetn); // 复位拉低通知所有需要感知复位的组件 reset_event.trigger(); // 等待复位释放 (posedge vif.resetn); reset_event.trigger(); end endtask endclass这里用uvm_event而不是简单的flag因为事件可以同时被多个对象等待而且同一个事件能反复触发。sequence里可以fork一个线程专门wait_trigger复位来了立刻响应主逻辑该收尾收尾该disable disable。3.2 sequence内部监听复位并优雅退出的模板stop_sequences()负责关上sequencer的调度闸门但sequence自己的业务逻辑可能正停在某个事务等待上。所以sequence里需要写一个“复位监听线程”一旦收到复位事件就让主业务逻辑走退出流程。我常用的模板是这样class base_reset_aware_seq extends uvm_sequence #(my_transaction); uvm_object_utils(base_reset_aware_seq) uvm_event reset_event; function new(string name base_reset_aware_seq); super.new(name); endfunction virtual task pre_start(); super.pre_start(); void(p_sequencer.try_get_reset_event(reset_event)); endtask virtual task body(); // 主逻辑 fork begin do_bus_transactions(); end begin // 监听复位事件 if (reset_event ! null) begin reset_event.wait_trigger(); end else begin wait (0); // 无事件时永不触发 end // 复位来了直接关闭业务 disable fork; end join_any endtask virtual task do_bus_transactions(); // 真正的sequence业务内容 repeat(10) begin uvm_do(req) end endtask endclass这个模板的核心是fork...join_any主业务线程和复位监听线程并行谁先结束谁决定整个body的走向。正常传输完成do_bus_transactions跑完body自然退出复位中途到了监听线程触发disable fork把主业务线程中还没有完成的部分停掉。要注意的是disable fork会连监听线程自己一起disable掉所以通常需要在disable fork之后立刻做一些清理再让body继续结束。这个模板里join_any之后的代码可以用来统一做清理比直接在两个分支内部各自收尾更可控。3.3 driver/sequencer在复位期间的gating策略除了sequence层复位发生的那一刻driver可能正在驱动一个transaction到总线而这个transaction在半路被DUT复位破坏了。此时如果driver还继续驱动剩余节拍总线上会出现不符合时序协议的波形。我的一般做法是driver的run_phase里用wait_for_reset事件去中断当前驱动循环。class my_driver extends uvm_driver #(my_transaction); uvm_component_utils(my_driver) uvm_event reset_event; virtual task run_phase(uvm_phase phase); forever begin seq_item_port.get_next_item(req); fork begin // 正常驱动事务 drive_transaction(req); seq_item_port.item_done(); end begin // 监听复位 if (reset_event ! null) reset_event.wait_trigger(); // 复位期间停止驱动并将当前事务标记为未能完成 seq_item_port.item_done(); end join_any disable fork; end endtask endclassdriver在复位发生时不再等drive_transaction结束而是直接调item_done()释放当前item这样sequencer侧才能知道当前事务已经回收对应sequence的finish_item才不会一直卡着。这个协同是很多人忘记补的一环只停sequence不停driver最终sequence还是会在wait_for_item_done处等死。4. 实测中的三个“stop_sequences()没有生效”的排查链路4.1 sequence卡在get_responsestop_sequences()无可奈何第一个案例来自一个AHB转APB桥的验证环境。现象是复位释放后APB配置sequence还在发送但已经完全不响应了。我在配置sequence的body里加了打印发现它停在了get_response()上也就是sequence把transaction发给driver之后在等待DUT侧返回response。这时候我已经在reset_phase里调了sequencer.stop_sequences()但sequence依然不退出原因正如前文所说get_response()等待的是driver通过put_response()返回的数据这条路和sequencer的grant是完全独立的。stop_sequences()关不掉这种等待。排查链路是先确认sequence停在哪个语句用$display或波形定位→ 确认它是wait_for_grant还是get_response等待→ 如果是get_response等待说明停止逻辑不能依赖sequencer需要在sequence内部创建监听复位事件的fork线程在复位触发时disable掉get_response等待。修复后sequence的body是这样virtual task body(); fork begin req my_transaction::type_id::create(req); start_item(req); req.addr ...; finish_item(req); get_response(rsp); // 可能长时间等待 end begin reset_event.wait_trigger(); disable fork; end join_any endtask4.2 同一个sequencer上旧sequence残留导致新sequence启动异常第二个案例是“复位后的第二个sequence永远不会开始”。代码里确实调用了stop_sequences()也确实启动了新sequence但新sequence就像被禁言了一样一个包都不发。排查最后发现问题不在stop_sequences()本身而是旧的sequence对象在复位前raise了phase_ready_to_end()或持有objection复位后objection没有drop干净导致新sequence所在的phase根本没进入真正可以执行的状态。此时底层逻辑一直在等objection为0新sequence的body()都没有被调用。这种场景下stop_sequences()可以帮助旧sequence退出仲裁但objection生命周期是另一个维度必须确保sequence的pre_body()/post_body()里有对应的raise/drop配对。我的排查习惯是在reset发生前后打印m_phase的objection计数并能很快定位到是哪个sequence泄漏了objection。如果实在无法定位可以在sequence基类的post_body()里做一次保护性dropvirtual task post_body(); if (get_sequence_state() UVM_STOPPED) begin // 说明是被停止的做一些清理 end uvm_phase phase m_starting_phase; if (phase ! null) phase.drop_objection(this); endtask但这只是兜底根本办法还是把reset监听线程和主业务线程的join逻辑理清楚确保每条路径都会执行到post_body。4.3 寄存器模型镜像值未复位导致复位后前门访问混乱第三个案例非常典型和stop_sequences()看起来没关系但也是在环境复位时会一起爆出来的复位后寄存器模型镜像值还是复位前的值这时sequence通过前门去写一个寄存器寄存器模型自动预测或显式predict的结果与DUT实际值不一致随即导致后续sequence里的读回校验全部失败。我当时的排查很痛苦因为sequence的事务本身没有问题驱动也没有问题DUT上电寄存器默认值也是对的所有东西单看都正常。最后对比波形才发现register model的mirored值没有在复位后更新导致sequence依赖mirror值做的比较全部错位。处理的思路是同步进行复位结束后不要只调用stop_sequences()还要触发一次寄存器模型的reset把镜像值重新从DUT侧读回来或按复位值批量更新。这里也简述一下如果环境里有使用uvm_reg_model建议在复位事件处理中加一步// 复位后主动更新镜像 reg_model.reset(); if (!reg_model.get_auto_predict()) begin // 如果关闭了auto predict一般需要显式调用sample或预测 reg_model.predict_all_values(); endstop_sequences()只是让激励停止而寄存器模型和reference model的复位状态必须由环境设计者主动维护。这两个动作经常被放在一起讨论是因为它们都是“环境复位”的一部分一个管激励一个管状态同步。5. 落地方案与代码清单5.1 一个模块级环境中的复位sequence完整示例写一个整体模板供读者参考我通常会在每个测试用例的reset_phase里执行“硬件复位环境停止”的操作而不是把复位逻辑散落在各个sequence里。class base_test extends uvm_test; uvm_component_utils(base_test) reset_agent rst_agent; my_env env; virtual task reset_phase(uvm_phase phase); phase.raise_objection(phase); // 1. 先拉硬件复位一段时间 rst_agent.reset_dut(); // 2. 停止sequencer上的所有sequence env.sequencer.stop_sequences(); // 3. 刷新DUT和参考模型之间的状态同步 env.ref_model.reset(); env.reg_model.reset(); // 4. 给一点时间让环境中进程完成退出 #1us; phase.drop_objection(phase); endtask // 后续启动sequence直接用新的sequence实例 virtual task main_phase(uvm_phase phase); phase.raise_objection(phase); my_vseq vseq my_vseq::type_id::create(vseq); vseq.start(env.sequencer); phase.drop_objection(phase); endtask endclass这个模板的关键在于顺序先做硬件复位再停sequence再做状态同步。顺序反了容易出问题。比如先stop_sequences()再拉硬件复位那么driver收到复位时可能还在处理一个旧事务环境里的复位监听线程接到的通知顺序也会乱。另外每次复位后启动的sequence建议用新的实例不要复用已经被stop过的对象。虽然UVM对sequence的可重用性做了一些设计但我在实战中发现新实例能避免大量“状态残留”类的疑难杂症。5.2 关于reset和UVM_PHASE_STARTED的一点经验再提一个关于复位时机和phase的细节如果环境中使用了uvm_sequence_library或者自动化sequence调度reset_phase和main_phase的边界往往并不像代码看上去那么清晰。reset_phase可能在配置阶段之前就触发了也可能在DUT时钟还没起来时紧急到来。所以不要在phase回调里假设“此刻sequencer一定是空闲的”。我在一个环境中遇到过在reset_phase里调stop_sequences()时main_phase的sequence已经使用phase.raise_objection()抢跑了。这样stop_sequences()之后新sequence已经获得grant并开始执行停止的标志对它没起到作用。处理方式是在sequence自己的body里也监听reset_event而不只是依赖phase顺序。这也是为什么我强调环境复位要当成一个“广播事件”而不是“phase顺序事件”来处理。stop_sequences()负责的是sequencer层面的停止但真正的优雅退出是sequence、driver、reference model、register model各司其职的配合。5.3 什么场景下要优先用stop_sequences()什么场景该改用kill_sequences()根据我自己的项目经验列一个简单的决策建议重传场景复位来了但之后可能还需要在这个sequencer上跑别的sequence用stop_sequences()让旧sequence温和退出新sequence之后能正常启动。死等场景sequence已经确定永远等不到response比如DUT不复位响应sequence卡在get_response优先kill_sequences()或结合sequence内的disable fork直接退出。单个sequence异常场景只想停掉某个跑飞了的sequence用kill()或stop()不要全局停止整个sequencer。复位释放后马上要恢复激励的场景如果复位后紧接着就要重新触发相同类型的sequence最好用新sequence实例并在新实例里重新获取reset_event的句柄。kill_sequences()在使用时要格外小心它会把所有已注册的sequence直接标记为停止如果这些sequence内部还开着子线程可能造成句柄未回收或回调未执行。我一般只在debug阶段或确认业务逻辑不会再恢复的情况下用。6. 最后分享几个能让环境复位少出问题的习惯6.1 给sequence统一增加“复位感知”能力前面给出的base_reset_aware_seq建议直接放在验证环境的sequence基类里。所有业务sequence都继承这个基类强制它们具备监听复位的能力。这样做的好处是后续新增的sequence天然继承这套保护不需要写sequence的同事每次都重复实现reset监听逻辑。6.2 复位后加一段“静默窗口”复位释放后DUT的时钟和内部状态需要一段时间稳定。不要立刻让新sequence开始发事务。我习惯在reset_phase的最后或者main_phase的前端插入一个固定的静默时间比如#1us或等待DUT的某个ready信号拉高。这个等待同样通过phase objection保护不能直接丢一个delay了事。6.3 在关键位置打印sequence状态当sequence异常不退出时打印是最好用的定位工具。可以在wait_for_grant、start_item、get_response前后打印sequence的get_state()。UVM_STOPPED是stop_sequences()之后sequence常见的状态如果反复看到这个状态说明停止逻辑已经触发如果始终停在UVM_ACTIVE说明复位事件根本没有传到sequence里。6.4 把stop_sequences()调用放在driver复位监听之后我个人的习惯是stop_sequences()永远在driver对复位信号完成“通道复位”之后调用。也就是说driver要先感知复位并停掉当前驱动事务再让sequencer停止调度。否则sequencer把grant发给driver前的那一拍driver可能已经在等一个永远不会来的item了。这些点看上去零碎但都是我在真实项目中一个坑一个坑趟出来的。环境复位的处理永远不是某一行代码的功劳而是hub上每个组件协同完成的一个“协议”。stop_sequences()是这个协议里的一环用对了能让环境复位干净利落用错了只会让问题更加隐蔽希望你少走这些弯路。
返回列表