ARTICLE DETAIL

资讯详情

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

UVM句柄与对象:子类父类赋值、$cast用法及多态机制详解

UVM句柄与对象:子类父类赋值、$cast用法及多态机制详解 咱们搞UVM验证的平时在社区里回答新手问题十个里有八个最后都能归到“句柄和对象”这俩概念上。尤其是子类和父类的句柄赋值那真是重灾区。不少人写sequence、写driver的时候明明代码看着没问题一编译就报类型不匹配或者一跑仿真就空指针崩溃查半天发现是对子类父类的句柄关系理解有偏差。这个主题我早就想系统写一篇了。今天就把UVM实战里最常见的子类父类句柄和对象问题从底层概念到实际代码一次性掰开揉碎讲清楚。这篇东西适合刚接触UVM的验证新人也适合那些已经写了几个testbench、但遇到类型转换还是心里发虚的朋友。看完你至少能搞明白三件事为什么父类句柄能指向子类对象、为什么反向不行、以及$cast到底该怎么用才不出幺蛾子。1. 先把地基打好对象和句柄很多人第一步就理解歪了1.1 一个最常见的口误把“对象的句柄”说成“对象本身”在SystemVerilog里类实例化用的是newmy_transaction tr; tr new(tr);第一行声明了一个变量tr它的类型是my_transaction。第二行在内存里真正创建了一个my_transaction对象然后把对象的地址交给了tr。关键在于tr这个变量里存的不是对象本身而是对象的“地址”或者叫“引用”。在SystemVerilog语境里这个引用就叫句柄handle。好多人嘴上说“tr这个对象怎么怎么样”实际上说的是“tr这个句柄所指向的那个对象怎么怎么样”。平时说话无所谓但一旦牵扯到赋值、传递、类型转换这个区分就致命了。我习惯用一个类比句柄就是通讯录里的联系人卡片。tr是卡片的名字卡片上写着一个地址顺着这个地址才能找到真正的对象实体。你把卡片复印一份给别人人家拿到的也是同一个地址找到的还是同一个实体——你并没有把实体复制一份。1.2 句柄赋值的本质复制的不是对象是“联系方式”理解了句柄存的是地址很多现象就顺理成章了。看这段代码my_transaction tr1; my_transaction tr2; tr1 new(tr1); tr2 tr1;tr2 tr1执行之后内存里依然只有一个对象但tr1和tr2两个句柄都指向它。你用tr2改了对象的某个字段tr1再去看字段已经变了。这在UVM里太常见了——一个sequence_item在sequence里创建、randomize之后句柄被一路传递到driversequence和driver手里拿的是同一个对象任何一方修改字段另一方全会看到。这个特性和C的指针很像但SystemVerilog做了不少封装不用手动管内存释放。也正因为有了自动垃圾回收很多新手更不注意句柄的生命周期反而埋下隐患。比如某个句柄还在被别的地方引用你却以为它已经“没了”这种问题后面在排查章节我会专门讲。1.3 new出来的是什么对象在内存里占了一块地句柄只是门牌号继续深挖一点。new做的事有两件分配内存、执行构造函数。构造函数里你可以给成员变量赋初值。但不管构造函数多复杂new返回给调用者的始终是那一个句柄值。UVM里有一个很容易被忽略的点create方法。我们写自定义的transaction、sequence、component时经常用uvm_object_utils宏注册然后用type_id::create来创建对象。很多人只知道“这是工厂机制比直接new好”但不理解create本质上也是先分配内存再返回句柄。它比new多做的是让工厂有机会根据type override返回一个新类型的对象——这在后面讲多态时会有大用。1.4 动态类型与静态类型同一个句柄两个“身份”这是理解子类父类问题的核心中的核心。一句话总结句柄的声明类型静态类型决定了你能用这个句柄调用哪些方法和访问哪些成员句柄实际指向的对象类型动态类型决定了运行的时候这些方法到底怎么执行。class base_trans; int a; virtual function void display(); $display(base_trans: a%0d, a); endfunction endclass class ext_trans extends base_trans; int b; virtual function void display(); $display(ext_trans: a%0d, b%0d, a, b); endfunction endclass base_trans btr; ext_trans e1; e1 new(); btr e1; // 合法静态类型是父类动态类型是子类这里btr的静态类型是base_trans所以你只能通过btr访问a和display()不能直接访问b。但btr的动态类型是ext_trans所以当你调用btr.display()时执行的是ext_trans版本的方法。这就是虚方法多态的威力。注意上面的写法是合法的很多入门教程会说“父类句柄可以指向子类对象”指的就是这个。这句话严格说不够严谨准确讲是“父类声明的句柄可以保存子类对象的引用”。2. 类型兼容性子类和父类之间哪些能赋值哪些是坑2.1 向上赋值子类→父类为什么安全以及怎么用子类对象“是一个”父类对象。ext_trans是base_trans的一种所以ext_trans对象天然具备base_trans的所有字段和方法。把子类对象的句柄赋给父类句柄相当于把一份更详细的名片塞进一个只看基础信息的卡槽里。卡槽能读到的内容一定不会超出子类对象实际拥有的内容所以这一步永远安全。向上赋值在UVM里极其普遍。比如sequence里产生的事务对象通常是你自定义的子类类型但通过start_item、finish_item发送时内部接口的形参往往是uvm_sequence_item类型——这就是隐式的子类转父类。class my_seq extends uvm_sequence #(my_transaction); virtual task body(); my_transaction mtx; mtx my_transaction::type_id::create(mtx); start_item(mtx); // ... finish_item(mtx); endtask endclassstart_item的形参是uvm_sequence_item itemmtx是my_transaction而my_transaction继承自uvm_sequence_item所以这里发生了向上赋值完全合法。反过来你就要小心了。UVM内建代码里大量出现父类句柄目的就是让框架代码不依赖具体的事务类型只管通用的控制和握手。你自定义的类型信息通过句柄传递到框架后框架并不关心你的具体类型它只调用基类定义好的方法。等到需要恢复具体类型、访问你的扩展字段时就需要接下来的向下转换。2.2 向下赋值父类→子类为什么编译都过不了假如driver的run_phase里收到一个uvm_sequence_item类型的句柄但你明确知道它实际是一个my_transaction想直接用my_transaction的字段class my_driver extends uvm_driver #(my_transaction); virtual task run_phase(uvm_phase phase); my_transaction mtx; // 从seq_item_port拿到的是 uvm_sequence_item 类型不这里实际是 my_transaction // 因为参数化拿到的是 my_transaction但换个场景就不一定了 endtask endclass这里因为my_driver参数化了my_transactionget_next_item返回的就是my_transaction不需要转。真正的经典场景是uvm_sequence_item类型的句柄从generic接口传进来你想恢复成自定义类型。如果编译器允许你直接把父类句柄赋给子类句柄就会出大事假设父类句柄实际指向的就是一个纯base_trans对象它根本没有b这个字段强行赋给子类句柄后子类句柄会以为对象里有b一访问就越界了。SystemVerilog是类型安全的语言这种隐患在编译期就杜绝了。所以下面这行代码直接编译报错ext_trans e2; base_trans btr new(); // 对象真是 base_trans e2 btr; // 编译错误不能把 base_trans 句柄赋给 ext_trans 句柄2.3 $cast给“向下赋值”上一把安全锁既然编译器不让你直接向下赋值运行时又想恢复具体类型那就用$cast。$cast的完整形式是function int $cast(目标句柄, 源句柄);它做的事情是检查源句柄的动态类型是不是目标类型的子类或相同类型。如果是就把源句柄赋值给目标句柄返回非0如果不是保持目标句柄原值返回0。ext_trans ext; base_trans btr; btr new(); // 动态类型是 base_trans if ($cast(ext, btr)) $display(cast ok); else $display(cast failed);这里$cast会失败因为btr指向的对象不是ext_trans类型。改成下面这样就成功了ext new(); btr ext; // 向上赋值 base_trans btr2; if ($cast(ext, btr)) // 成功 $display(cast ok, ext.b%0d, ext.b);需要注意$cast既可以用作函数返回int也可以当作任务调用。当$cast以任务形式调用时转换失败会直接报运行时错误并终止仿真。函数形式不会终止只是返回0让你自己处理。在UVM验证环境里我强烈建议全部用函数形式检查返回值因为仿真跑着跑着因为一个类型不匹配就崩掉体验极差而且定位麻烦。2.4 避坑别用C风格的强制类型转换替代$castSystemVerilog里还有一类操作符就是base_trans(obj)这种类型转换语法。C语言背景的人容易顺手写成ext_trans(btr)以为和C的(ext_trans*)一样。但在面向对象类型上这种写法很危险。对于某些内建类型它可能执行值转换但对于类句柄它不会做动态类型检查只是静态地把类型重新解释。一旦实际对象类型不对后面用起来就是定时炸弹。在UVM的环境里你只需要记住一个原则**类类型的句柄转换一律用$cast函数形式不要用类型转换操作符。**这是无数血泪换来的经验。2.5 一个实践案例get_next_item返回后怎么处理下面写一个最常见的完整场景。自定义一个事务类型扩展了基类的字段class my_transaction extends uvm_sequence_item; rand bit [31:0] addr; rand bit [31:0] data; rand bit [3:0] burst_len; uvm_object_utils_begin(my_transaction) uvm_field_int(addr, UVM_ALL_ON) uvm_field_int(data, UVM_ALL_ON) uvm_field_int(burst_len, UVM_ALL_ON) uvm_object_utils_end function new(string name my_transaction); super.new(name); endfunction endclassdriver里如果你不想用参数化的uvm_driver #(my_transaction)而是直接继承uvm_driver #(uvm_sequence_item)那拿到的句柄就是基类类型的class my_driver extends uvm_driver #(uvm_sequence_item); virtual task run_phase(uvm_phase phase); uvm_sequence_item item; my_transaction mtx; forever begin seq_item_port.get_next_item(item); if (!$cast(mtx, item)) begin uvm_error(DRV, $sformatf(cannot cast to my_transaction, type%s, item.get_type_name())) seq_item_port.item_done(); continue; end // 到这里 mtx 才是可用的 my_transaction 句柄 drive_bus(mtx); seq_item_port.item_done(); end endtask endclass这段代码在转换失败时给了一个明确的错误报告而不是让仿真默默崩溃。get_type_name()在这里尤其好用能直接打印出对象实际的类型名帮你快速定位是哪里发来一个“不速之客”。3. 多态的底层逻辑为什么子类对象能被当成父类用还能保持“本色”3.1 virtual方法绑定的是动态类型非virtual方法绑定的是静态类型多态要建立在虚方法上。基类里声明了virtual function的方法通过父类句柄调用时会按照对象的动态类型去执行子类版本。但非虚方法就不一样它是按句柄的静态类型来决定的。这在UVM里最常见的影响就是print、copy、compare、pack这些UVM内建方法。uvm_object基类里这些方法都是virtual的所以factory机制和report机制拿到你的对象句柄时尽管静态类型是uvm_object或uvm_sequence_item调用print()走的仍然是你子类通过uvm_object_utils_begin注册的field automation打印逻辑。这也是为什么uvm_object_utils宏那么重要它不光注册了工厂还实现了get_type_name、create、get_object_type等方法让你的类在通过父类句柄操作时也能“认识自己”。3.2 一个实验父类句柄调用被隐藏的非虚方法再看这个代码class base_trans; virtual function void info(); $display(base info); endfunction function void whoami(); $display(base whoami); endfunction endclass class ext_trans extends base_trans; virtual function void info(); $display(ext info); endfunction function void whoami(); $display(ext whoami); endfunction endclass base_trans btr; ext new(); btr ext; btr.info(); // 输出 ext info虚方法按动态类型 btr.whoami(); // 输出 base whoami非虚方法按静态类型这个实验几乎所有UVM新手都值得亲手跑一遍。跑完你就明白为什么UVM源码里凡是要支持多态的方法清一色加virtual。你在继承uvm_sequence、uvm_component、uvm_sequence_item并重写body、build_phase、print等方法时本身就在享受多态带来的便利。3.3 为什么UVM框架敢用父类句柄传来传去却从来不崩一个sequence被sequence机制调度的时候uvm_sequencer_base里存的是uvm_sequence句柄一个sequence_item在uvm_port_base里传来传去的时候接口类型是uvm_sequence_item。UVM框架代码完全是基于基类写的但实际跑起来时所有被调用的方法都是具体子类重写过的。框架不关心你的事务有什么字段、你的sequence怎么生成数据它只负责通用流程流程里需要扩展行为时就通过虚方法回调到你的实现。这套“框架层用基类句柄用户层用子类对象”的设计是整个UVM方法学的骨架。换句话说搞清楚句柄和对象的关系你不仅是在学语言语法而是在理解UVM框架本身。4. $cast和句柄检查的实战现场sequence、driver、config_db、寄存器模型4.1 场景一sequence发送自定义事务driver还原类型前面已经给了代码。这里再补充一个关键细节start_item和finish_item之间sequence手里的事务对象句柄类型是你自定义的my_transaction但底层经过uvm_sequence_item类型转发。如果sequence里对同一个句柄做了randomize然后driver侧$cast回来修改了字段sequence这边也看得到——因为句柄指向的还是同一个对象。如果你希望driver侧改完不影响sequence侧那就得用copy或clone这又是另外一个话题对象的深拷贝等下次单独写一篇。4.2 场景二config_db里set/get本质是父类句柄的传递uvm_config_db #(uvm_object)是UVM里最常用的配置通道。set的时候不管你是uvm_object、uvm_sequence_item还是uvm_component都会被当作uvm_object句柄存进配置库。get的时候同样返回uvm_object。这时候你如果用子类句柄去接收就必然要用$cast。class env_config extends uvm_object; int is_active; uvm_object_utils_begin(env_config) uvm_field_int(is_active, UVM_ALL_ON) uvm_object_utils_end endclass // 在某个component里set env_config cfg env_config::type_id::create(cfg); uvm_config_db #(uvm_object)::set(this, *, env_cfg, cfg); // 在另一个component里get uvm_object obj; env_config my_cfg; uvm_config_db #(uvm_object)::get(this, , env_cfg, obj); if (!$cast(my_cfg, obj)) begin uvm_fatal(CFG, get env_cfg failed, config type mismatch) end注意如果用uvm_config_db #(env_config)::get传递的句柄类型已经锁定为env_configUVM内部会帮你做类型检查但底层实现的原理依然是句柄赋值只是框架替你做了$cast的事情。4.3 场景三寄存器模型镜像值一样离不开句柄和类型UVM寄存器模型里uvm_reg_field的get_mirrored_value这类操作底层访问的是寄存器模型的镜像mirror值。镜像值本质上存储在寄存器模型的对象之中而每个uvm_reg的子类对象、每个uvm_reg_field对象都是先通过工厂创建再以基类句柄的形式被加入队列、映射表。你写.get()、.predict()、.mirror()时背后全是句柄在数据库里搬来搬去。如果哪天你在寄存器模型里遇到“拿到了对象但类型不对”的报错多半是某个寄存器类忘了注册或者从reg_block的map里取出来的句柄没有正确$cast成你期望的寄存器类型。4.4 场景四对象创建时期与句柄传递的配合type_id::create创建的对象在UVM工厂中可以被子类override。举个例子class my_base_seq_item extends uvm_sequence_item; uvm_object_utils(my_base_seq_item) endclass class my_ext_seq_item extends my_base_seq_item; uvm_object_utils(my_ext_seq_item) endclass // 在test class里 function void build_phase(uvm_phase phase); my_base_seq_item::type_id::set_type_override(my_ext_seq_item::get_type()); super.build_phase(phase); endfunction之后如果你的sequence里用my_base_seq_item::type_id::create(item)来创建对象工厂实际返回的是my_ext_seq_item类型的对象。你拿到的句柄静态类型仍是my_base_seq_item但动态类型已经是子类了。这就是override的魔法同时也是各种类型转换问题的温床。遇到这类问题一定要记住$cast检查的是动态类型不是静态类型。4.5 场景五组件层次句柄parent不是你想的那样在uvm_component的构造函数里parent参数的类型是uvm_component。你把一个my_env传进去它在基类里被保存为uvm_component句柄。等到UVM遍历层次结构时用的是uvm_component的方法get_parent()、get_child()返回的全是uvm_component句柄。如果有一天你在某段代码里拿到一个uvm_component句柄但你确定它实际是my_env想调用my_env里自定义的方法怎么办还是$cast。UVM里这种操作不常写因为用uvm_top.print_topology()、uvm_root::get()这类接口时通常用不到扩展类型但一旦你写全局查找某个特定组件的工具方法就绕不开这个。5. 高频问题速查与排查技巧以后别再被这种bug折磨了5.1 一张表看清症状、原因和药方问题现象根本原因解决办法编译报错cannot assign parent handle to child handle直接把父类句柄赋给子类句柄编译器拒绝用$cast函数形式检查返回值运行时崩溃报错指向访问某个字段句柄实际指向的不是你期望的子类对象强行访问扩展字段转换前加$cast检查打印get_type_name()确认动态类型$cast失败但仿真继续跑后面出现空指针忽略了$cast的返回值目标句柄未被赋值每次$cast后判断返回值失败就报错并给出处理通过父类句柄调用方法执行的是基类版本方法没有声明为virtual按静态类型绑定检查方法定义需要多态就加virtualconfig_db get回来的对象强制转换后抛异常配置类型不匹配存进去的是另一种对象使用带类型的config_dbget后加$cast检查打印实际类型factory override后行为没有按子类执行忘了用create直接用了new或override类型设置时机太晚统一用type_id::create在build_phase早期设置override修改了driver里的对象字段sequence侧也变了两个句柄指向同一个对象这是正常现象如果不想共享使用copy()创建副本再修改sequence_item传了8个包后卡住和句柄对象关系不大多为item_done/get_next_item握手不匹配检查driver是否每个item都调用了item_done5.2 排查套路一打印类型比你瞎猜强一百倍遇到句柄类型相关的诡异问题第一反应别去翻代码先打印对象动态类型。UVM对象一般都有get_type_name()配合$sformatf打印到日志里一眼就能看出当前句柄指向的真实类型是什么。$display(item type is %s, item.get_type_name());如果打印出来是uvm_sequence_item而不是你的my_transaction说明factory override没设置对或者创建处就不是你的类型。类型一确认问题就解决了一半。5.3 排查套路二检查句柄非空但别忘了“空句柄”和“坏句柄”的区别SystemVerilog中句柄默认值是null。要判断句柄是否有效用if (mtx ! null)。但这只能判断“有没有存地址”判断不了“地址上是不是你要的类型”。所以完整套路是if (item null) begin uvm_error(DRV, item is null) end else if (!$cast(mtx, item)) begin uvm_error(DRV, $sformatf(cast fail, type%s, item.get_type_name())) end else begin drive_bus(mtx); end很多人只查了空没查类型结果类型不对的时候还是炸。两种检查都做才能真正安全。5.4 排查套路三句柄赋值为null不等于对象被销毁在SystemVerilog里一个对象如果没有被任何句柄引用垃圾回收机制会回收它的内存。但注意如果有两个句柄指向同一个对象你把其中一个赋值为null对象并不会被销毁因为另一个句柄还引用着它。反过来有些场景你以为某个对象还在内存里其实已经被回收了——比如某个句柄是局部变量作用域结束后再也没人引用它后面你拿着一个旧的、已经失效的句柄去访问轻则读到垃圾数据重则崩溃。UVM环境虽然很少手动管理内存但长仿真里、动态创建大量sequence_item的场景下句柄生命周期管理不当确实会造成“玄学”bug。建议对于生命周期较长的对象用UVM的uvm_object_string_pool或者config_db统一管理别让句柄散落在各个task里。5.5 关于“不回respond但也只能发八个包”这类sequence问题的延伸有一些sequence问题看起来像是句柄对象的锅实际上是握手协议没对齐。比如sequence发出item后一直不收到response仿真卡在8个包左右这多半是driver和sequencer之间的item_done/put_response调用不匹配或者sequence里get_response逻辑写错。这类问题的排查思路是打印sequence的m_sequencer状态、item的sequence_id和transaction_id确认每条item的ownership是否被正确传递。句柄和对象的话题虽然不能直接解决握手问题但如果你想深入理解sequence机制里item如何被传递、谁持有句柄、谁负责释放今天讲的这些基础就派上用场了。5.6 对象的copy与clone最容易触发句柄误用的地方UVM对象的copy方法尤其是深拷贝要特别小心。默认的uvm_object_utils_begin/end宏里定义的copy对每一个uvm_field声明的成员做的是“值拷贝”。对于句柄类型的成员比如你定义了一个my_sub_object sub_obj;并注册为uvm_object字段默认拷贝行为可能是浅拷贝——也就是新对象的sub_obj句柄和原对象的sub_obj句柄指向同一个子对象。修改其中一个另一个也变。如果你希望两个对象完全独立需要手动重写copy方法或者给子对象单独实现copy然后在父类的copy里单独处理。很多UVM环境里出了“改了A对象的字段B对象也变了”这种诡异问题十有八九就是这个浅拷贝在作怪。6. 从这个问题延伸出去还有哪些“看起来简单实际上天天坑人”的点写到这里其实还有一个更大的话题藏着没展开就是UVM对象的print、compare、pack在继承关系下的行为。很多时候子类对象通过父类句柄操作默认的compare会因为类的类型不同而返回不匹配哪怕所有字段值都一样。这是因为UVM的compare默认会检查m_object的类型是否相同。遇到跨类型比较一般需要重写compare或者在调用前先$cast成同一类型。这些点每一个单拿出来都够写一篇长文但它们的根基都是今天讲的句柄和对象。我在实际项目里见过太多人遇到这类问题第一反应是去查UVM源码结果越查越晕其实只要把“静态类型决定编译期可见性动态类型决定运行时行为$cast做安全检查”这三句话刻在脑子里90%的问题都能一眼看穿。最后分享一点个人体会我在刚学UVM那阵子也栽过跟头。当时写一个寄存器测试sequence里创建了一个寄存器类型的对象通过基类句柄传到scoreboardscoreboard里想调用它独有的方法直接用了C语言风格的强转仿真跑了一个多小时才崩定位花了两天。后来养成一个习惯凡是涉及类句柄的转换一律写$cast并查返回值凡是涉及对象传递先确认“这个句柄是谁创建的、谁还持有它、它的动态类型是什么”。这个习惯让我在之后的项目里少加了无数个夜班。建议每个人在搭UVM环境的时候都建一个自己的“类型转换/句柄检查”模板函数或者宏把常见的$cast加类型打印封装一下用起来顺手也安全。如果你自己在验证环境里踩过什么和句柄对象相关的奇葩坑或者对copy深拷贝、compare类型匹配、sequence握手item所有权这些话题感兴趣欢迎留言后面可以继续一篇一篇展开。
返回列表