的正确用法与常见误区)
在一次做GPIO控制器验证的项目里我写了一个复位用例先让DUT跑起来再拉低复位然后重新初始化整个UVM环境。最开始我图省事直接在virtual sequence里调用了m_sequencer.stop_sequences()以为这样就能把当前所有sequence清掉、从头再来。结果sequence里的uvm_info还在刷driver还在稳步输出transaction整个环境像没收到复位指令一样。后来翻了UVM源码又踩了好几个坑才算把“UVM环境复位”这件事彻底理顺。这篇就把stop_sequences()这个API背后的机制、正确用法和常见误区一次性讲清楚。stop_sequences()是UVM sequencer提供的一个函数作用是停止当前挂载在该sequencer上的所有sequence。它和你在仿真里直接拉低复位信号是两回事它只负责“让sequence停下来”不管driver手里的transaction、不管TLM FIFO里积压的数据、也不管寄存器模型里的镜像值。如果你把这几个环节漏了环境复位就会出现各种奇怪的现象要么sequence没停住要么复完位之后一跑就卡死要么比对结果全乱。下面我一个一个拆开说。1. stop_sequences()到底停住了什么1.1 先搞清sequence是怎么“挂”在sequencer上的理解stop_sequences()之前先要理解UVM里sequence和sequencer的协作关系。一个sequence要被执行第一步是调用start()由虚拟sequence或者test指定挂在哪个sequencer上。一旦start成功sequencer内部就会把这个sequence加入到一个动态数组m_sequences里并在仲裁、发送item时查询这个数组。sequence真正发送数据时走的是start_item()和finish_item()这对API。start_item()会进入仲裁流程sequence先通过wait_for_grant()等待sequencer许可拿到许可之后sequence把item通过send_request()提交给sequencer最后调用finish_item()等待本次item处理完成。整个流程中sequencer维护的是每个sequence的“执行状态”而m_sequences里记录的就是当前还活着、还在等待仲裁或被调度的sequence。所以stop_sequences()这个函数名里的“sequences”指的就是这些记录在m_sequences数组里的活跃sequence。它的作用就是遍历这个数组把每一个sequence都执行一次终止操作然后把它们从数组里清掉。有一点容易混淆stop_sequences()不是一个task它不等待任何东西。它只负责把sequence标记为停止、从sequencer的调度表里移除。对于已经在driver手里、已经被取走的item它管不着。这个特性决定了它只能作为复位流程里的一环而不是全部。1.2 源码层面的行为与边界UVM源码里uvm_sequencer_base::stop_sequences()的实现逻辑大致是这样的不同版本略有差异但思路一致function void uvm_sequencer_base::stop_sequences(); while (m_sequences.size() 0) begin uvm_sequence_base seq m_sequences[$]; seq.kill(); end endfunctionkill()是uvm_sequence_base提供的方法它会清理sequence内部的状态、回收已经分配的资源并最终让启动这个sequence的start()调用返回。但注意这个“返回”不是立刻发生的它需要当前正在执行的代码块让出控制权。换句话说如果你的sequence body里有一个forever循环并且没有检查任何退出条件调用kill()之后它也不会立刻消失必须等循环体自身有机会执行到某个同步点比如下一次start_item()、get_response()等才能正真退出。这是很多人第一次用stop_sequences()后产生困惑的核心原因他们以为这是一个“一键强杀”实际上它更像一个“礼貌的逐客令”终止流程已经发出了但sequence要整理完手头的事才能真正消失。stop_sequences()的边界条件也值得记一下它不会停止已经被driver取走、正在执行中的item。driver手里的那份transaction会继续跑到item_done()为止。它不会清空sequencer和driver之间通信用的TLM FIFO。FIFO里残存的item会在复位后继续被读到。它不会重置寄存器模型、不会清空scoreboard里的期望值队列、不会重置reference model的状态。它对virtual sequencer上的“虚拟sequence”没有直接效果如果你只在virtual sequencer上调用底层各个agent sequencer上的sequence并不会被停掉。理解了这些边界后面设计复位流程时就不会踩坑了。1.3 stop_sequences()和kill()、stop_sequence()怎么选很多初学者分不清stop_sequences()、kill()和stop_sequence()。简单梳理一下方法所属对象作用范围适用场景stop_sequences()sequencer当前sequencer上所有活跃sequence需要全局停止、环境复位stop_sequence(seq)sequencer指定的单个sequence只停止某一个sequence保留其他sequence继续跑kill()sequence本身当前sequence自己在sequence内部主动退出或者配合监控逻辑主动终止自己实际项目里我用的最多的是stop_sequences()其次是在sequence内部加退出条件用kill()的情况很少。因为kill()从内部调用时如果处理不好容易在清理资源时和正在进行的start_item()冲突反而引入新问题。2. 设计一套正确的UVM环境复位流程2.1 复位前先给sequence留一条“体面的退路”既然stop_sequences()不是强杀那我们就要在sequence里设计合理的退出机制让它收到复位信号后能自己走完退出流程。我的做法是给公共基类sequence加一个复位事件监听所有业务sequence都继承这个基类。class base_sequence extends uvm_sequence #(base_item); uvm_object_utils(base_sequence) protected bit reset_flag; function new(string name base_sequence); super.new(name); endfunction virtual task pre_body(); super.pre_body(); reset_flag 0; fork begin // 监听全局复位事件 if (cfg ! null) begin cfg.reset_evt.wait_on(); reset_flag 1; end end join_none endtask virtual task guarded_start_item(base_item req); if (reset_flag) begin uvm_info(get_type_name(), reset detected, skip start_item, UVM_LOW) return; end start_item(req); endtask endclass这样做的好处是当复位事件到来时正在等待仲裁、等待response的sequence会立刻被唤醒并设置reset_flag下一次循环里它会主动跳过start_item()自然退出。即便stop_sequences()晚一拍才执行sequence自己也不会继续产生新事务了。注意pre_body()里fork出来的监听任务必须用join_none否则sequence还没开始发数据就先阻塞在监听上了。监听逻辑本身要在pre_body()结束之前启动这样从第一个start_item()开始就能感知到复位事件。2.2 复位中stop_sequences()、清队列、reset寄存器模型三步走完整的环境复位不能只调stop_sequences()。我总结了一个三段式流程项目里一直这么用第一步停止所有sequence。通过顶层virtual sequencer遍历底层所有sequencer逐个调用stop_sequences()。这里有一个细节virtual sequencer本身没有实际的sequence调度职责真正的sequence挂在各个agent sequencer上所以必须逐个agent去停。task env_reset::do_reset(); // 1. 发复位时序 reset_if.rst_n 1b0; #(RESET_CYCLE * clk_period); reset_if.rst_n 1b1; // 2. 停止所有底层sequencer上的sequence env.agent1.sequencer.stop_sequences(); env.agent2.sequencer.stop_sequences(); // 3. 等待driver当前item执行完用事件同步不要用固定延时 env.agent1.driver.done_evt.wait_on(); env.agent2.driver.done_evt.wait_on(); // 4. 清空所有TLM FIFO残留 env.agent1.seqr.req_fifo.flush(); env.agent2.seqr.req_fifo.flush(); // 5. 重置寄存器模型 reg_model.reset(); // 6. 清空scoreboard/reference model环境状态 env.scoreboard.reset_state(); endtask第二步等待driver把当前正在处理的item收尾。这一步很多人忽略导致的问题是stop_sequences()虽然停了sequence但driver手里那个item还在执行如果你立刻开始新的sequence两者会在同一个sequencer上抢仲裁产生难以定位的竞态。用done_evt.wait_on()比#100ns这种固定延时靠谱得多。第三步清理队列和寄存器模型。uvm_tlm_fifo自带了flush()方法可以一次性丢弃所有未读数据。寄存器模型这里需要单独强调一下因为和“镜像值”强相关。2.3 复位后寄存器模型镜像值不同步的坑UVM寄存器模型RAL有一个非常重要的概念镜像值mirror value。它模拟的是软件视角下寄存器里应该是什么值。默认情况下寄存器模型里的镜像值和DUT实际寄存器值是一致的因为环境在启动时通常会做一次reg_model.reset()初始同步。但复位一拉低DUT寄存器被硬件复位成了默认值寄存器模型里的镜像值却还停留在复位之前的数值。如果这时候有一个sequence通过寄存器模型去读寄存器读之前模型会对比镜像值和期望值发现不一致就报错或者更隐蔽的是模型内部根据镜像值做一些判断比如判断某个bit是否置位从而决定后续流程分支结果判断用的是旧值整个测试方向就带偏了。所以环境复位流程里reg_model.reset()这一步绝对不能省。如果你的寄存器模型是通过uvm_reg_block::default_map访问的还要注意reset之后要重新调用reg_model.set_sequencer()和reg_model.set_auto_predict()之类的配置因为有些实现里reset会把这些关联关系也清掉。此外如果DUT存在异步复位、不复位所有寄存器之类的特性只调reg_model.reset()可能还不够最保险的做法是复位后主动对关键寄存器执行一次mirror_read()把真实值同步回来。mirror读操作会实际发起总线读虽然慢一点但能保证镜像值绝对准确。3. 实操过程从“stop_sequences()没停住”到“十分钟定位”3.1 错误调用导致的三类典型现象我把自己和同事踩过的问题归成三类很典型第一类在sequence的body里调用m_sequencer.stop_sequences()。sequence在执行stop_sequences()时自己也在m_sequences数组里调用后它自己也会被kill掉。很多人在virtual sequence的body里写这个结果virtual sequence刚把第一个底层sequence发出去就被自己给停了整个测试行为完全不符合预期。正确做法是从test层或者一个独立的环境控制模块发起stop而不是从sequence自己里面发起。第二类只在virtual sequencer上调用。有些agent sequencer确实是挂在virtual sequencer下面的但virtual sequencer本身没有实际的sequence列表它只是作为一个路由层。只调virtual_seqr.stop_sequences()底层agent sequencer上的sequence毫发无损。必须遍历到底层实际管调度的 sequencer。第三类用fork/join_none启动sequence立刻在fork后面调用stop。sequence还没来得及在sequencer上完成注册stop就已经执行完了。因为sequence的注册是在start()进入后才发生的这两者之间存在一个极短的时序窗口。我建议在start sequence之后先等待一拍比如等待一个时钟沿或者一个小延时再执行stop。不要小看这个时序窗口在线仿真的时候它几乎不出现一旦切到带时序的仿真或者加入延时控制问题就会被放大。3.2 现场复现不回responsesequence最多只能发八个包很多做UVM验证的同学应该都听过一句话“driver不回response的话sequence最多只能发八个包就卡住了。”这句话在实际工程里确实有对应的现象。我帮同事定位过一个类似问题他的sequence在body里循环发item代码写得很简单repeat (20) begin uvm_create(req) start_item(req); finish_item(req); // 没有调用 get_response() enddriver那边也正常在不断取item但sequence跑了8个包后就不再发起新的请求了。问题出在sequence和sequencer之间的通信通道上。UVM的sequence和sequencer底层是通过两个TLM FIFO通信的一个传requeest一个传response。不同版本、不同封装的默认FIFO深度不一样有些实现默认给到8一旦积压到FIFO容量上限继续发送的一方就会被阻塞。为什么“不回response”会和“最多八个包”有关因为sequence每次调用finish_item()时底层有一个response返回值机制。尽管你代码里没有显式调用get_response()通信通道本身仍然被这次交互占用了一部分资源。driver一直不回response这条通道的积压就越来越多直到达到容量上限sequence侧再想发新包就发不出去了。注意这里我要多说一句网上很多文章喜欢把这个“8”当作一个UVM的固定阈值去背但这个数字更多取决于你使用的UVM库实现和TLM FIFO配置不是一个标准答案。在有的实现里默认可能是1有的可能是8。真正的排查思路不是记住“8”这个数字而是意识到“sequence发不出去新item”和“底层FIFO满了”之间的因果关系。定位方法也很简单在卡住的位置暂停仿真查看m_req_fifo和m_rsp_fifo的深度或者直接在sequence里打开UVM_HIGH级别的打印看它还停在哪个调用点。如果停在finish_item内部基本就是FIFO阻塞。复位环境下这问题会变得尤其突出stop_sequences()停掉了sequence但FIFO里的积压数据还在复位后新的sequence一启动老数据和新数据混在一起紧接着就是死锁或者错误比对。3.3 寄存器模型镜像值错误导致的连锁反应还有一个和复位强绑定的问题复位后寄存器模型镜像值和实际寄存器不一致。有一次我的环境复位后寄存器模型对RTL发起了一次读操作读取到的值和镜像值不一致寄存器模型直接打印了UVM_ERROR。第一反应以为是DUT功能bug后来逐步排查发现原因在复位流程没做reg_model.reset()。复位前DUT寄存器被软件配置成非默认值复位信号一拉低硬件寄存器回到默认值。寄存器模型不知道这件事它的镜像值还是复位前那个非默认值。此时发起读操作模型认为“期望读到镜像值的状态”RTL返回实际复位值两边对不上校验失败。更麻烦的是连锁反应如果后续sequence的branch条件里用到了寄存器模型读取出来的数据而这个数据来自错误的镜像值那整个流程都会沿着错误的路径走最后报出一堆莫名其妙的比对错误。你花一天时间查RTL最后发现是环境自己的状态没复位。所以我在每个复位序列的末尾都会专门加一步寄存器模型同步并且同步完成后打印一个摘要级信息比如镜像值和实际值对比方便后续排查。4. 环境复位场景的常见问题排查与避坑清单4.1 现象-原因对照表现象可能原因解决方向调用stop_sequences()后sequence还在打印sequence body内没有检查退出标志kill之后仍在跑增加复位事件监听让sequence主动退出复位后重新启动sequence直接卡死TLM FIFO里有残留数据新sequence和旧数据竞争复位时对所有req/rsp FIFO执行flush()复位后寄存器读操作报镜像值错误没有调用reg_model.reset()镜像值还停留在复位前复位流程中加入reg_model.reset()并mirror_read关键寄存器复位后scoreboard大量误报expected队列没有清空旧期望和新transaction混合在scoreboard挂reset方法清空expected队列和内部计数器某些底层sequence没被停止只调了virtual sequencer的stop_sequences()遍历所有底层agent sequencer逐个调用sequence自己把自己kill了在sequence的body里调用了stop_sequences()把stop的调用点移到test层或环境控制模块复位后driver还在输出transactionstop_sequences()不处理driver手中已经取走的item用driver完成事件同步等待当前item执行完再继续virtual sequence复位后状态不对虚拟sequence自身在resume时没有重置内部标志在virtual sequence里增加复位逻辑重跑前重置所有局部变量4.2 架构建议不要把环境复位做成一个普通sequence最后说一个架构层面的经验。我见过不少人把复位流程写成一个sequence然后在test里像发普通事务一样启动它。短期的确能用但项目跑大了以后会出问题复位sequence和其他业务sequence共享同一个sequencer仲裁关系一旦处理不好就会互相干扰。我的建议是把环境复位从sequence体系里剥离出来作为一个独立的环境控制接口。它既不是sequence也不挂在sequencer上而是由test层直接调用内部负责发起复位时序、停止sequence、清空队列、重置寄存器模型、清理scoreboard状态。这样整个复位流程是“环境级”的操作不依赖底层sequence的调度逻辑也清晰得多。配合一个独立的reset_agent来监测复位信号的实际时序也是一个稳妥做法。reset_agent通过uvm_event在复位发生前、复位结束等关键节点发出同步事件环境里所有需要感知复位的组件sequence、driver、scoreboard、寄存器模型都监听事件并做各自的清理动作。比在每一个sequence里都加fork监听要省心得多也避免了事件遗漏。4.3 一句重要提醒复位顺序不能颠倒整个复位流程里顺序很关键。最佳顺序是先让复位事件通知出去、然后等sequence自己退出、再调用stop_sequences确保清干净、接着等driver处理完手头item、flush队列、最后重置寄存器模型和scoreboard。这个顺序我是试过错才总结出来的。如果你先把reg_model.reset()做了再等sequence退出sequence里正在进行的访问可能已经基于旧的寄存器配置复位后访问路径变了结果全乱。如果你先flush队列再等sequence退出sequence可能又在flush之后塞了一个新item进来等于白flush。说白了复位环境的核心逻辑只有一个先“停”再“清”最后“重置”。顺序对了问题少一半。我个人在实际操作中的体会是UVM环境复位最坑人的地方不在于stop_sequences()本身不够强而在于它给使用者一种“已经复位了”的错觉。真正严谨的复位流程需要你手动去处理那些散落在各处的状态。把这几个环节串起来以后复位用例的稳定性会提升不少至少不会每次跑到第二次复位就报一堆莫名奇妙的错。最后再分享一个小技巧复位流程执行完之后不要立刻启动新sequence先做一个短暂的空转检查确保所有sequencer上已经没有旧sequence残留。可以用uvm_sequencer_base的is_busy()接口轮询一下等它返回0再往下走。这个方法看起来不起眼但能帮你把上面提到的很多“隐藏状态”一次性暴露出来省掉无数排查时间。