
1. 为什么验证平台需要替换能力做验证时间长了你会发现一个很尴尬的局面UVM平台一旦搭好所有人都默认这套环境就是长这样的。可实际上不同测试用例对平台的需求往往是互相冲突的。拿我做过的一个APB-UART项目来说功能测试希望driver老老实实按协议把激励发出去但异常测试希望driver在发送过程中突然插入一个奇怪的毛刺功能测试要求reference model把每一拍数据都算得明明白白但跑回归时又希望换一个精简模型来加速仿真。这两个需求放在同一个平台里靠if/else去判断代码会变得没法看而且每加一个测试用例就要改一遍环境代码麻烦不说还容易把已经稳定的环境改挂。UVM的Factory机制说白了就是给这类问题开了一扇门你想要替换平台的某一部分不必去动环境代码只用在测试用例里面告诉Factory把A类型替换成B类型平台创建组件时自然就会拿到B的实例。这也是说覆盖与替换的艺术的原因——它解决的不是怎么写代码的问题而是怎么让验证平台具备柔性的问题。覆盖override与替换replace是这个机制的两大核心动作。覆盖指的是针对某一个注册过的类型或实例路径注册一条替代规则替换发生在create()真正执行的时候Factory发现存在匹配的替代规则就创建替代类型而不是原始类型。理解这条链路比背API本身重要得多因为后面所有调试本质都是在追这条链路上的某个环节。这篇文章适合三类人一类是刚学UVM、对type_id::create知其然不知其所以然的同学一类是已经写了几个测试用例但遇到为什么我override没生效这类问题只能靠瞎试的工程师还有一类是想把验证环境从能用提升到好复用、好扩展的验证负责人。内容会涉及原理、源码逻辑、实战案例和踩坑记录尽量一次性把Factory机制讲透。2. Factory注册表create()后面藏着什么2.1 注册宏到底做了什么先看最基础的一段代码class my_driver extends uvm_driver #(my_transaction); uvm_component_utils(my_driver) ... endclass很多初学者以为uvm_component_utils就是给Factory登记个名字实际上它展开之后做的事情远不止如此。它会在类内部定义一个类型专用的注册器类型type_id这个注册器继承了uvm_component_registry#(my_driver, my_driver)并且提供两个关键的静态函数create()返回uvm_component和get()返回注册器本身。同时uvm_component_utils还在类的定义处通过静态初始化方式把my_driver的名字和工厂包装器注册到全局的Factory注册表里。这就是为什么用了这个宏类才能被Factory管理的根本原因。如果你写了一个类没有加任何Utils宏然后企图用type_id::create去创建它编译器会直接报错因为type_id根本不存在同样你也无法通过set_type_override_by_type去覆盖一个没有注册的类。注册表本身是一个uvm_factory实例它在UVM环境启动时由uvm_coreservice_t创建。注册表里存放的是从类型名字字符串到注册器对象的映射。这里有个细节值得注意注册表的键是字符串也就是宏里写的那个名字比如my_driver不是类名本身。所以同名注册会发生什么UVM的规则是后者覆盖前者而不是报错。我在一次代码集成中见过两个不同模块都注册了monitor这个名字结果后来注册的那个把前面那个顶掉了平台行为完全变了排查了很久才定位到。2.2 create()和new()的差别是路线问题理解Factory机制必须先分清create()和new()。new()是SystemVerilog构造对象的固有方式直接分配内存并调用构造函数不受任何外部机制干预。create()则分两步走先通过Factory注册表查找类型以及检查有没有匹配的覆盖规则再由Factory决定实际创建哪个类最后调用那个类的new()。用大白话说new()是我要什么就new什么create()是Factory说该给你什么才new什么。你写的是my_driver::type_id::create(drv, this)但如果预先设置了my_better_driver覆盖my_driver最终创建出来的实例是my_better_driver变量的静态类型仍然是my_driver基类。这里之所以能成立全靠SystemVerilog的继承和多态——my_better_driver必须是my_driver的子类否则Factory会在覆盖检查时报告类型不兼容。很多人在排错时习惯直接在build_phase里打断点看对象类型然后发现是my_better_driver而不是my_driver立刻以为是代码写错了。其实看到这个结果恰恰说明Factory机制在高高兴兴地工作。真正要检查的是我设置的覆盖路径到底匹不匹配。2.3 从源码角度看一次create的完整流程以UVM 1.2源码为例当调用my_driver::type_id::create(drv, this)时实际进入的是uvm_component_registry::create它会调用全局工厂的create_component_by_type(get(), ...)。create_component_by_type会执行以下几步查找匹配的覆盖记录overrideFactory内部维护着一个覆盖记录的列表包括类型覆盖和实例覆盖按优先级排列。它会遍历所有记录找出第一个同时满足原始类型匹配和实例路径匹配的记录。根据覆盖记录决定最终实例类型如果没有找到覆盖记录直接使用请求的原始类型如果找到了使用覆盖记录指定的替代类型。检查类型的兼容性确认替代类型是原始类型的子类如果发现替代类型和原始类型没有继承关系会报告uvm_fatal任务直接终止。调用create_component函数真正创建实例并调用new(name, parent)完成对象构造。另外UVM还提供了create_component_by_name这个入口它只通过字符串名字进行查找。这种机制的注册信息不在编译期就能检查直到运行时才报错用的场景更多是脚本生成代码或动态配置。在写常规验证环境时优先使用type_id::create这一类类型安全的方式。顺带说一句如果设置了UVM_FACTORY_TRACE开关每次create调用时UVM都会打印一条跟踪信息告诉你请求了什么类型、经过哪些覆盖检查、最终创建了什么类型。这是一个排查为什么override没生效的神器后面我专门讲。3. 覆盖机制的三层境界类型覆盖、实例覆盖、代理覆盖3.1 四个API一张表讲清UVM提供了四类覆盖设置函数按按键方式和目标粒度区分函数按键方式覆盖粒度典型场景set_type_override_by_type类型全局类型级整个test平台中所有该类型的组件全部替换set_type_override_by_name名字字符串全局类型级从命令行或配置文件传入类型名set_inst_override_by_type类型实例路径级只替换特定路径下的某个组件set_inst_override_by_name名字字符串实例路径级针对具体层次路径做定制类型覆盖好理解一旦设定整个平台中请求这个类型的所有create都会返回替代类型。例如在某个test中执行set_type_override_by_type(my_driver::get_type(), my_err_driver::get_type())那么环境中所有请求my_driver的地方都会创建my_err_driver不管driver在哪里、叫什么名字。实例覆盖则多了一个路径参数比如set_inst_override_by_type(uvm_test_top.env0.agent.driver, apb_driver::get_type(), apb_bad_driver::get_type());这条规则只对uvm_test_top.env0.agent.driver这一个实例生效env1下面的driver不受影响。路径支持UVM的层次通配符比如*可以匹配任意一段名字uvm_test_top.*.driver就能匹配任意agent下的driver。这个设计在VIP复用场景下特别有用同一套IP环境例化了多个实例只替换其中一个。3.2 覆盖的匹配优先级不是后写谁听谁UVM的覆盖查找有一个固定的优先级规则实例覆盖优先于类型覆盖。这一点非常容易踩坑。假设环境里有某个driver你同时设置了在base_test的build_phase里设置类型覆盖my_driver-my_super_driver在某一个test里设置实例覆盖uvm_test_top.env.driver-my_err_driver此时创建这个driver最终得到的是my_err_driver而不是my_super_driver因为实例覆盖的优先级永远高于类型覆盖。哪怕你先写实例覆盖、后写类型覆盖实例覆盖依然占上风。UVM不是按时间先后覆盖前一条规则而是按规则类型决定查找顺序的。这带来一个实用经验如果想要被某个特定test的配置最终决定最灵活的做法是设置实例覆盖如果要做一轮全平台的统一替换就用类型覆盖但要确认环境里没有挂任何实例覆盖冲突。3.3 命令行覆盖比改代码更敏捷set_type_override_by_name这类API的意义在于它可以让覆盖信息从外部输入——比如命令行参数uvm_set_type_overridemy_driver,my_err_driver或者通过UVM的run_test加上UVM自带的全局配置。UVM提供了一个实用的工具uvm_cmdline_processor可以解析uvm_set_inst_override和uvm_set_type_override格式的命令行参数。这个功能在回归脚本里非常有用比如某个nightly回归你想快速试验一个替代组件不用改任何测试代码只需要在仿真命令里加一个参数./simv UVM_TESTNAMEbase_test uvm_set_type_overrideapb_driver,apb_err_driver跑完看结果不满意就删掉一点不进代码仓库。对验证工程师来说这是Factory机制里最低成本、高收益的用法。3.4 覆盖的代理类思想所谓代理覆盖其实指的是通过继承加覆写的方式设计替代类让它作为原组件在特定场景下的代理人。比如你想要一个driver在发送数据前自动插入延迟不必去改原始driver的代码可以定义class apb_driver_with_delay extends apb_driver; uvm_component_utils(apb_driver_with_delay) int extra_delay; virtual task send_data(...); // 先插入额外延迟 repeat(extra_delay) (posedge vif.pclk); super.send_data(...); endtask endclass然后通过覆盖机制把它换上去。这样做的好处是原始driver保持干净不受任何测试场景污染维护成本大幅降低。这也是替换的艺术中最核心的日常实践不是一味地造新轮子而是通过继承和覆盖在基类功能之上灵活扩展。不过要说清楚的一点是代理覆盖不适合所有情况。如果替代类和原始类的行为差异过大比如连端口、接口、事务类型都变了那就不是覆盖能解决的范畴了应该考虑重新设计组件结构。Factory的覆盖需要替代类型是原类型的子类型而且父类引用只能调用父类已有的方法新增的方法在通过基类句柄访问时是不可见的除非配合$cast进行下行转换。因此做代理覆盖时一定得思考基类句柄用得到哪些能力设计子类方法时尽量基于覆写基类的虚方法。4. 实战在项目中完成一次driver替换与错误注入4.1 场景设定APB UART验证平台我用一个真实的项目场景来说明。假设有一个APB-UART验证环境环境结构大致是uvm_test_topenvapb_agent内含apb_driver、apb_monitor、apb_sequenceruart_agentscoreboardreference_model功能测试已经跑得很好了但现在领导要求验证UART模块在APB总线出现超时未响应时的行为。正常情况下apb_driver每笔传输都能拿到ready信号环境不会产生超时场景。我需要替换driver让它在特定时刻吞掉ready信号模拟总线卡死。4.2 第一步创建替代driver子类class apb_err_driver extends apb_driver; uvm_component_utils(apb_err_driver) bit inject_timeout; function new(string name, uvm_component parent); super.new(name, parent); endfunction virtual task send_transfer(apb_transfer tr); if (inject_timeout) begin // 等待超过协议规定的最大等待周期 repeat (16) (posedge vif.pclk); end super.send_transfer(tr); endtask endclass注意apb_err_driver继承了apb_driver并且覆写了send_transfer方法。这样当Factory创建driver时实际创建的是apb_err_driver由于方法被覆写异常行为自然就被注入到原有的驱动流程之中。在这里apb_driver中send_transfer必须声明为virtual task否则覆写不会生效这是SystemVerilog多态的基础。4.3 第二步在test中设置覆盖在apb_timeout_test的build_phase中设置覆盖class apb_timeout_test extends base_test; uvm_component_utils(apb_timeout_test) function void build_phase(uvm_phase phase); super.build_phase(phase); set_inst_override_by_type(uvm_test_top.env.apb_agent.driver, apb_driver::get_type(), apb_err_driver::get_type()); endfunction endclass这里必须注意一个关键点覆盖规则要在create之前设置。UVM的build_phase是从顶层往下执行的test的build_phase先执行然后才执行env的build_phase所以把set_inst_override_by_type放在test的build_phase中能保证env创建driver时规则已经就位。如果你放错了地方比如放在connect_phase里面driver早就建完了任何override都无济于事而且不会有任何报错提示很容易造成怎么改都不生效的迷惑现象。4.4 第三步验证覆盖是否生效设好覆盖之后要做到心里有数可以在test的end_of_elaboration_phase打印一下Factory的配置信息function void end_of_elaboration_phase(uvm_phase phase); uvm_factory f uvm_factory::get(); f.print(); endfunction或者更精确地检查具体实例的类型apb_driver drv; $cast(drv, uvm_test_top.env.apb_agent.driver); if (drv null) uvm_error(TEST, driver cast failed!) else uvm_info(TEST, driver override applied successfully, UVM_LOW)uvm_factory::print()的输出会列出当前所有的override记录一目了然。如果这里看不到你的规则说明覆盖根本没有设置成功应该回头检查路径写法、类型参数等。4.5 第四步sequence粒度的替换除了替换driver本身Factory还经常用在一个更细的粒度上——sequence。UVM中sequence item通常也是通过start_itemfinish_item来创建的而且sequence item的创建同样经过Factory。这意味着你可以为某个sequence item类型设置覆盖让sequencer把原始item替换成带错误属性的子类item。比如定义apb_unaligned_trans作为apb_trans的子类在构造时强制aligned0然后通过类型覆盖使得某个测试用例中所有创建的apb_trans都变成非对齐类型。这个技巧用于生成错误激励非常高效不用改sequence代码只需在test里加一行覆盖即可。从这些实战环节你应该能感受到Factory机制的价值。替换不是在一个类里写死替不替换而是在不同的测试场景里动态决策。这让测试意图和环境实现解耦也让我们这些做验证的人能更快写出高针对性的用例。5. 覆盖机制的边界与经典坑位5.1 坑一没有注册的类无法覆盖这是最基础也最普遍的坑。定义了一个类忘了加uvm_component_utils或uvm_object_utils然后设置覆盖时编译通过运行时却报错或静默不生效。原因就是Factory的注册表里根本没有这个类型后续任何覆盖查找都无法匹配。要特别注意一个场景你定义了一个基类base_driver里面加了uvm_component_utils(base_driver)继承它的sub_driver没有加任何宏。然后在test里设置sub_driver覆盖base_driver。编译没问题运行时大概率报uvm_fatal提示类型不在factory中或者不是子类型。原因是sub_driver没有把自己注册进Factory类型信息不完整。sub_driver作为base_driver的子类它当然可以通过base_driver的注册器创建但如果你想让它被显式地覆盖或由名称查找那还得给它自己的注册身份。补充一点UVM1.2之后提供了uvm_component_utils_begin/uvm_component_utils_end这种可以额外注册字段的宏变体但基本注册逻辑一样扩展字段主要用于uvm_field_automation的print、copy、compare支持不注册就会影响到这些操作的实现。字段自动化不在这篇文章主线但值得知道。5.2 坑二new()绕过Factory还有一个非常常见的假问题——平台里某处用new()直接创建组件同时你给它设置了override结果发现完全没效果。这不是UVM的bug而是创建路径的问题。new()是对象构造的必经之路但它本身不经过FactoryFactory不会拦截new()调用。想让Override生效只要用type_id::create或uvm_factory的create_object_by_type等接口创建对象。一些新手自己写的辅助类或者在某个看起来无关紧要的utility函数里直接new了一个component都会绕过Factory机制。一旦环境规模变大这种隐藏的new很难发现。建议在代码review时留意所有new(如果它是组件或者可被覆盖的业务对象必须改成create。这也是UVM平台规范里最容易被忽略的一条工程纪律。5.3 坑三覆盖设置时机晚于create前面提到过覆盖规则必须在create执行前注册。但实际工程环境中这个问题会藏得非常深。举一个我真实遇到的案例一个virtual_sequencer在connect_phase里创建了某个内部对象这种设计本身就不太好但因为历史原因保留了下来而我在test的build_phase里设置了覆盖。理论上build_phase早于connect_phase覆盖应该生效。如果我们在test里用super.build_phase(phase)之后设置覆盖然后virtual_sequencer的build_phase会在test之后执行所以能生效没问题。但如果在test里用uvm_config_db搭配Factory做延迟绑定或是在run_phase里通过工厂创建对象那就要特别小心时机了。一旦在run_phase才创建覆盖设置即使在check_phase也能生效但如果你在connect_phase之后、run_phase之前设置覆盖也可以只要早于那个create调用。所以关键规律是确保你的覆盖规则在目标对象创建之前进入Factory任何晚于create的覆盖都是无效的。5.4 坑四继承关系不对导致uvm_fatalUVM的Factory对覆盖类型有一个强约束替代类型必须是原始类型的派生类。这既是保障也是限制。如果你设置覆盖时搞反了方向比如父类覆盖子类或者两个毫无继承关系的类互相覆盖UVM运行时会出现uvm_fatal错误仿真直接终止。为什么UVM要这么强硬因为create()返回的句柄需要向上转型为原始类型如果类型之间没有继承关系句柄赋值就无法通过SystemVerilog的类型检查即便强行通过也会在后续使用中引发不可预知的崩溃。所以它不是不通融而是在为你挡一道大概率会出事的隐患。这里分享一个技巧在设置覆盖前如果类型比较复杂可以在test里用$cast试一下兼容性或者先在零散测试中打印类型名确认继承关系清晰后再加override。虽然多一步检查但在大型VIP上能省下不少后面偏头痛级别的时间。5.5 坑五只看报错不看跟踪信息遇到override不生效时很多人习惯性先翻代码一翻就是大半天。其实UVM给我们提供了很直接的调试工具UVM_FACTORY_TRACE。在仿真命令中加上这个参数后每次Factory执行创建流程都会在日志里打印类似下面这样的信息UVM_FACTORY_TRACE: Creating object of type apb_driver UVM_FACTORY_TRACE: Searching for overrides... UVM_FACTORY_TRACE: No overriding type found UVM_FACTORY_TRACE: Creating object of type apb_driver如果你的覆盖规则没被匹配到日志里会看到的是一条Searching for overrides...后面跟的是原始类型。根据这条链路可以快速定位问题所在到底是规则没设置上还是路径不匹配还是类型没注册。这里要说个使用技巧UVM_FACTORY_TRACE会输出大量日志整轮跑完文件巨大建议配合UVM_VERBOSITYUVM_MEDIUM过滤掉无关消息或者只在小用例中打开定位完再关。5.6 坑六误以为override能修改已创建对象有些人会把Factory的覆盖理解成运行时替换对象也就是对象创建完之后把现有的实例直接变成另一个类的实例。这是错误的理解。覆盖只影响将来由Factory创建的实例已经创建出来的对象无论后来怎么增加覆盖规则它都不会变。通俗地讲Factory相当于一个工厂流水线override相当于给流水线换了一个模具。流水线上已经生产出来的产品不会因为这个动作而改变下一个产品才会按新模具生成。了解这个机制你就不会在run_phase中期想着覆盖一个正在运行的组件来改变当前行为而是应该提前规划好覆盖规则或者改用配置参数控制行为。5.7 坑七通配符路径使用不当set_inst_override的路径通配符很方便但也容易出错。*只能匹配一层名字比如*.driver可以匹配env0.agent.driver但不能匹配env0.agent[0].driver这种中间多了一层的路径。如果你要匹配任意层数UVM路径规则没有**这种跨层通配符至少在标准UVM中是不支持的所以碰到多层嵌套就老老实实写完整路径或者用类型覆盖。还有一点UVM的实例路径在build_phase期间顶层不是uvm_test_top吗有可能你的环境里测试用例是base_test中例化的env设置的路径写uvm_test_top.env.driver而实际路径可能是uvm_test_top.tb_env.driver。复制路径的时候千万用uvm_top.print()打印组件树来核对不要手敲。6. 我的覆盖实践心得什么地方值得换什么地方不值得做了几年UVM验证最深的体会是Factory的覆盖不是万能药也不是越用越好而应该像设计模式一样有取舍地用。首先最适合用Factory替换的组件是那些行为变体很丰富、继承关系清晰的部件比如driver、monitor、scoreboard的比对策略、reference model的变体、sequence item的错误注入变体。这些东西天然适合用继承去表达差异再用Factory去动态选择。我通常会在test的build_phase里设置override而不是直接修改环境代码这样环境保持统一测试用例之间互相独立回归也好定位。其次不太适合用Factory替换的是那些承载配置信息、上下文的组件比如config对象、virtual interface容器。配置层面的差异更适合用uvm_config_db来传递不必非走类型覆盖。比如要让某个driver在某个test里多等待几个周期与其新建一个driver子类再覆盖不如直接在test里用uvm_config_db给driver设一个extra_delay参数driver内部判断。这样改动更小也更符合配置与行为分离的原则。换句话说如果差异可以靠参数表达就别靠继承和覆盖如果差异是行为流程层面的用覆盖更合理。另外要留意Factory机制带来的一个副作用调试困难程度会随着覆盖数量增加而上升。当环境里存在十几条覆盖规则时你创建一个对象最终得到的类型可能和代码里写的那一行type_id::create完全不搭边读代码的人会非常迷惑。所以我建议给覆盖规则的设置制定一个统一规范所有override集中放在测试用例或专门的配置类中并且在end_of_elaboration_phase统一打印一次factory状态这样代码的可读性和可维护性都有保障。关于性能可能有人担心Factory机制会不会拖慢仿真。实际上相比整个验证平台的运行开销Factory查找的开销极其微小主要发生在build_phase的组件创建阶段对整体仿真性能几乎可以忽略。所以完全不必为了性能去绕过Factory、恢复用new()——那是因小失大。保持一致的创建风格比追求那一点微小的性能收益更重要。最后分享一个我自己总结的覆盖使用三问这个替换是行为层面还是参数层面参数层面优先config_db。这个替换是针对全局类型还是某个特定实例全局用类型覆盖局部用实例覆盖。这个替换的派生类能完美兼容基类句柄的调用吗如果不能就说明设计边界有问题该重构而不是硬覆盖。凭这三问我在项目里规避了很多看起来能用、后面维护时炸锅的设计。Factory机制本身不难理解真正体现水平的地方在于什么时候用、怎么用、以及怎么让别人也能一眼看懂你的覆盖意图。从工程可维护性的角度看这比任何API细节都值得花时间。