ARTICLE DETAIL

资讯详情

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

SystemVerilog句柄与对象:UVM中父子类转换与$cast实战解析

SystemVerilog句柄与对象:UVM中父子类转换与$cast实战解析 1. 先搞清楚你手里的“遥控器”和“电视机”句柄与对象的本质区别前几天在内部代码评审的时候我又看到一位刚转做UVM验证的同事写出了这种代码seq_item_port.item new my_transaction(); seq_item_port.item.data 100;乍一看没问题但跑到后面就发现data字段的值在自己的类里改得好好的发到driver端就变成了0。排查了一下午才发现问题出在他对“句柄”和“对象”这两个概念的理解上。这不是个别现象UVM面试里考的“子类和父类的句柄和对象问题”本质就是把SystemVerilog面向对象的基本功和UVM的实际使用场景结合起来。搞不懂这层关系后面看factory机制、sequence通信、寄存器模型全是懵的。1.1 SystemVerilog里的句柄到底是什么很多教材把句柄比喻成“遥控器”对象比喻成“电视机”。这个类比我很认同。有一条SystemVerilog声明my_transaction tr;tr就是一个遥控器但它还没指向任何电视机。这个时候tr的值是null你拿一个null遥控器去按按键调用方法系统直接给你抛空指针。而“电视机”是对象它只会在你执行new()之后才存在tr new();new()做了两件事在内存堆里创建了一个my_transaction类型的对象然后把tr这个遥控器的指针指向这块内存。所以对象有生命周期句柄没有生命周期句柄只是个搬运模板、通道。我在培训新人的时候喜欢在黑板上画两张图左边一张叫“对象”右边一张叫“句柄”然后反复强调一句话句柄决定你能“看到”什么接口对象决定实际“运行”的是什么逻辑。这个区分是解决一切父子类句柄问题的钥匙。1.2 “悬空句柄”和“空对象”每天都在踩的第一类坑先说悬空句柄。一个句柄指向的对象被销毁了或者赋值为null这个句柄就叫悬空句柄。SystemVerilog里如果句柄指向的对象被回收你再去访问就会报空指针异常。UVM里面很多组件是phase自动创建、自动清理的环境退出了组件还挂着这种情况你是能打印出来的。再说一个更隐蔽的“空对象”问题。很多人以为base_tran child_tran;这句执行完之后base_tran就有了child_tran的所有字段。错了。这行代码只是把base_tran这个遥控器的指针指向了child_tran指向的那个电视机。对象还是同一个对象只是多了一个遥控器。base_tran能调的方法仍然是base_tran静态类型声明类型里有的方法但它实际指向的对象类型是child_tran。这就是“编译期看句柄类型运行期看对象类型”这句话的由来。UVM里有一个高频出错的场景你在sequence里声明一个基类句柄然后用这个句柄去访问子类里自定义的字段。uvm_transaction t; my_transaction mt; t mt; // 编译通过向上转型 t.data 100; // 编译报错uvm_transaction里没有data成员编译报错不是SystemVerilog不聪明恰恰是它在保护你t的类型是uvm_transaction编译器只允许你通过这个句柄的“说明书”来调用成员。你非要拿一个没有data字段的说明书去操作一个其实有data字段的对象编译期就无法确认安全性。2. 父类句柄指向子类对象UVM多态为什么能成立2.1 向上转型的底层逻辑父类句柄指向子类对象在面向对象里叫向上转型。C、Java、SystemVerilog都有这个概念。为什么这个操作是安全的很简单子类继承了父类所有的成员和方法所以子类对象肯定具备父类期望的所有能力。你拿一个“通用遥控器”父类句柄去操作一台“高级电视机”子类对象通用按键高级电视机肯定都支持所以编译不报错、运行不出错。在UVM里这种结构无处不在。最典型的例子就是uvm_sequence_item和自定义事务类型。看这段代码class my_transaction extends uvm_sequence_item; rand bit [31:0] addr; rand bit [31:0] data; uvm_object_utils_begin(my_transaction) uvm_object_utils_end endclass class my_driver extends uvm_driver #(my_transaction); virtual task run_phase(uvm_phase phase); my_transaction req; seq_item_port.get_next_item(req); drive_pin(req); endtask endclassmy_driver是uvm_driver参数化的参数类型是my_transaction所以get_next_item拿到的句柄类型就是my_transaction一切都好。但如果参数化类型里传的是父类比如class my_driver extends uvm_driver #(uvm_sequence_item);问题就来了。你从seq_item_port里拿到的静态类型是uvm_sequence_item想访问addr、data编译直接报错。怎么解决先把父类句柄转成子类句柄再做访问。这就要用到$cast。2.2 虚方法virtual在UVM事务处理中的作用再往深一层说为什么UVM的很多方法都是virtual因为只有声明为virtual的方法才能实现“运行期绑定”。假设父类有一个虚方法class uvm_sequence_item; virtual function void do_print(); $display(base do_print); endfunction endclass class my_transaction extends uvm_sequence_item; virtual function void do_print(); $display(my_transaction do_print); endfunction endclass在这种情况下uvm_sequence_item t; my_transaction mt; mt new(); t mt; // 父类句柄指向子类对象 t.do_print(); // 输出的是 my_transaction do_print这就是多态。虽然t的静态类型是uvm_sequence_item但因为do_print是virtual系统在运行期会根据t实际指向的对象类型去调用方法所以调用的是子类版本。UVM的core方法像copy、compare、print、record、pack全是virtual。这就是为什么所有自定义事务类都要注册宏并且可以靠factory机制返回不同类型的对象。没有virtual也就没有UVM这套东西。你可以在任何一本UVM实战书里看到这句话的变形但它背后真正对应的是“句柄静态类型”和“对象动态类型”的分离。3. 子类句柄指向父类对象UVM验证中最常见的崩溃现场3.1 为什么直接赋值会编译报错与向上转型相对向下转型就是让子类句柄指向父类对象my_transaction mt; uvm_sequence_item t; t new uvm_sequence_item(); mt t; // 编译报错为什么SystemVerilog直接拦下这行想想就知道mt是一个具有addr、data等额外字段的子类遥控器它指向的却是一个没有这些字段的父类对象。你通过mt去访问addr运行时就是在访问一块不存在的内存。为了避免这种错误编译器禁止直接赋值。有人问我那我能不能强制转换SystemVerilog提供了类型转换符但它只适合一些转换比如int转bit、或某些可确认安全的情况。对于对象句柄千万别直接用静态转换符mt my_transaction(t); // 推荐不要这么干这句话在语法层面可能能过运行时可能直接就崩。因为它不会做任何检查单纯把t这块内存当成my_transaction来用。父类对象实际分配的内存比子类对象小访问超出范围的成员后果不可预期说不定能跑出野指针但不报错那才是真正可怕的地方问题最后会以奇怪的失败形式出现彻底干扰排查方向。3.2 $cast的正确使用姿势正确做法是用$castmy_transaction mt; uvm_sequence_item t; t new uvm_sequence_item(); if (!$cast(mt, t)) uvm_fatal(get_type_name(), cast failed!)$cast执行时会检查t实际指向的对象是不是my_transaction类型或者是不是my_transaction派生类型。如果是mt指向t指向的对象返回1不是返回0mt保持null。这样你永远不会拿到一个“类型错乱”的句柄。展开说一个细节$cast有两种用法一是不带if直接调用另一种是带if判断。我强烈建议工作中用带if的写法。理由很简单$cast(mt, t); // 如果失败直接致命错误终止仿真其实MSB风格的$cast在失败时也不会直接致命终止它会返回0并打印一条warning但是后续代码继续执行。如果你后面马上用mt访问子类字段且没有判空依然是空指针访问。加了if之后类型不对就走你设计的错误分支至少在日志里能看到清晰的报错原因。还有一个tricky的点$cast的第二参句柄如果本身是null$cast必然失败。所以不要指望一个null句柄能cast出一个正常对象来先判null再cast是双重保险。3.3 从sequence调用sequencer方法看实际应用这类向下转型在UVM里最常见的场景是sequence里想获取sequencer上自定义的字段或者方法。class my_sequence extends uvm_sequence #(my_transaction); task body(); my_sequencer my_seqr; if (!$cast(my_seqr, m_sequencer)) uvm_fatal(get_type_name(), sequencer type mismatch) my_seqr.custom_flag 1; endtask endclassm_sequencer的静态类型是uvm_sequencer_base是个根底层的类。实际环境里sequence挂载到的是一个具体化参数后的my_sequencer。没有$cast的话你拿m_sequencer这个“通用遥控器”去访问custom_flag编译就卡住了。加了$cast之后就安全了如果实际挂载的sequencer类型不匹配先在这个检查点把问题抛出来避免后面在更深处崩。这里有个面试常考的延伸问题为什么UVM中的sequence机制不直接从m_sequencer里取子类句柄呢答案就在于m_sequencer的声明类型是uvm_sequencer_baseUVM设计者不可能某个具体sequencer的字段编译进去所以只能靠$cast在运行时把类型“还原”。4. UVM工厂机制下的句柄与对象override背后发生了什么4.1 create和new的区别如果只聊句柄和对象不聊UVM的factory这文章就等于只讲了一半。很多人的语法基础知道$cast但把它和factory override混在一起出现问题时焦头烂额。先问一个问题下面这两个创建对象的写法真实的区别是什么my_transaction t1 new(); my_transaction t2 my_transaction::type_id::create(t2);new()就是直接给my_transaction分配内存、调用构造函数。而create()会先调用factory的查找逻辑根据注册时登记的type_name看当前环境里有没有发生过针对my_transaction的override。如果有它返回的可能是子类对象。换言之t2 create(t2);这句话执行完之后t2的静态类型还是my_transaction但它实际指向的对象类型可能是override之后的子类。这不正好就是“父类句柄指向子类对象”的实战版吗所以我在评审里经常看到这样的一个问题我明明override了一个类型但sequence里拿到的对象为什么还是老的一通排查后发现写代码的人在创建对象时用了new()而不是create()factory的override机制根本没机会介入。4.2 override是改了句柄还是换了对象我见过有人对override的理解是“把对象里的字段值改掉”。不是的。override的核心是在factory的注册表里把某个类型A的create请求以后都替换成返回类型B的对象。它换的是“对象类型”原始句柄类型不变。比如class my_ext_transaction extends my_transaction; rand bit error_insert_en; endclass class test_base extends uvm_test; function void build_phase(uvm_phase phase); my_transaction::type_id::set_type_override(my_ext_transaction::get_type()); endfunction endclass之后任何地方的my_transaction::type_id::create()实际得到的对象类型都是my_ext_transaction。但句柄类型仍然是my_transaction。想访问error_insert_en这个子类独有字段还是要先$cast一把。这一步是UVM里“句柄和对象不同一”最经典的应用场景。4.3 关键问题override和$cast谁先谁后这个问题容易让人迷糊实际项目中这两者常常同时出现。场景是这样的基类sequence里创建了一个my_transactionclass base_sequence extends uvm_sequence #(my_transaction); task body(); my_transaction req; req my_transaction::type_id::create(req); start_item(req); // ... endtask endclass因为test环境里做了overridereq实际指向的是my_ext_transaction对象。但base_sequence里声明的句柄是my_transaction直接想访问error_insert_en肯定编译不进去。难道要改base_sequence那就失去了用override的目的了。多数项目的做法是写一个扩展基类class ext_sequence extends base_sequence; task body(); my_ext_transaction req_ext; my_transaction req_base; // ... req_base my_transaction::type_id::create(req); if (!$cast(req_ext, req_base)) uvm_fatal(get_type_name(), cast failed after create) req_ext.error_insert_en 1; endtask endclass这里面有一个顺序要理清楚先create拿到对象然后$cast把基类句柄转成子类句柄再去访问子类独有字段。如果有人试图把$cast放在create之前那必然失败因为你面对的是一个null句柄。5. 我在实际项目中踩过的句柄/对象深坑5.1 copy与clone的浅拷贝问题UVM的事务类一般都会实现copy和clone。标准写法很多书里有但很少有人在书里强调clone返回的句柄静态类型是什么。看function uvm_object clone(); uvm_object tmp; tmp create(clone); tmp.copy(this); return tmp; endfunctionclone方法的返回值类型是uvm_object也就是父类。你用下面这段代码my_transaction t1 new(); my_transaction t2; t2 t1.clone(); // 报错clone()返回uvm_object编译必定报错。为什么UVM这么设计因为clone是写在uvm_object这个基类里的返回值只能是一个通用类型。如果你确定clone出来的对象就是my_transaction你需要做一次向下转型if (!$cast(t2, t1.clone())) uvm_fatal(get_type_name(), clone cast failed)这是我见过的新手最常踩的坑没有之一。说句题外话网上不少借鉴代码直接写my_transaction t2; t2 my_transaction(t1.clone());这种能跑通是运气好因为t1.clone()的实际对象确实是my_transaction类型静态转换没检查也就蒙混过关了。但如果t1被override成了别的子类这种写法就埋雷了。5.2 compare时的类型不匹配另一个高频问题出现在compare。两个句柄指向的对象类型不一致时对比结果总是失败。具体场景driver从sequencer拿到req之后向scoreboard发送了两个句柄一个是req另一个是某个expected transaction。实际上expected的创建路径可能经过了override实际类型是my_ext_transaction而req也是my_ext_transaction。两边实际对象类型相同但句柄声明类型不同比如一个声明为my_transaction一个声明为uvm_sequence_item。此时调用comparemy_transaction act; uvm_sequence_item exp; if (exp.compare(act)) // 能过吗比较的是两个对象的内容不是句柄类型所以能过。但如果你在compare里用了自定义方法比如在my_transaction里扩展了do_compare访问了一个父类里没有的成员virtual function bit do_compare(uvm_object rhs, uvm_comparer comparer); my_transaction rhs_t; if (!$cast(rhs_t, rhs)) return 0; return (super.do_compare(rhs, comparer) (this.ext_field rhs_t.ext_field)); endfunction这时如果rhs的实际对象不是my_transaction派生类型$cast失败compare直接返回0。这恰恰说明对象类型是否一致决定了compare的最终可靠程度。如果UVM内部compare不对类型做任何检查而你的do_compare里又没有$cast那么可能把不同类型的对象强行比较某些字段产生不可预期的结果。5.3 配置数据库config_db里存错类型的对象最后一个坑来自config_db。很多人在environment里这样写uvm_config_db#(uvm_sequence_item)::set(this, *, my_item, item);然后在driver里uvm_sequence_item item; uvm_config_db#(uvm_sequence_item)::get(this, , my_item, item);set进去的时候item实际是my_transaction类型。get出来item静态类型是uvm_sequence_item。继续想访问子类字段必须$cast。如果你set的静态类型和get的静态类型不一致情况会更复杂uvm_config_db#(my_transaction)::set(this, *, my_item, item); // set用子类静态类型 uvm_config_db#(uvm_sequence_item)::get(this, , my_item, item); // get用父类静态类型这会直接get失败因为config_db的查找匹配同时要求层次路径和参数类型完全匹配。我在实际项目中专门因为这个问题查了一个多小时最后才意识到set和get的静态类型必须一致加上$cast才能完成类型还原。6. 写代码时怎么从根本上避开这些坑6.1 声明句柄时尽量用贴近实际对象的类型很多人在UVM环境里图省事把一些本应具体化的句柄都声明成基类类型uvm_sequence_item item;然后后面到处$cast。这种代码看多了先从声明处开始调整思路。能明确知道是my_transaction的地方就直接声明为my_transaction能用参数化组件就用参数化组件比如uvm_driver#(my_transaction)。少数需要多态的地方才用基类句柄。这样能从源头上减少不必要的向下转型。6.2 覆盖override之前要问自己目标对象会不会变添加一个override之前先想一想环境中所有创建这个类型的位置。所有create对象的地方都没问题这是好事。但是如果你在基类sequence里声明了一个基类句柄override之后实际对象成了子类那就需要基类代码考虑后续向下转型。如果扩展的字段只是在driver侧被用到也许可以写在基类里做一个hook方法然后子类override这个方法而不必每个使用处都做$cast。这种设计导向终于让代码更干净也会减少很多类型转换的负担。6.3 打印日志时把“真实类型”打出来排查这类问题最好的利器就是打印对象get_type_name()。我经常在所有组件run_phase开始的第一行加一个调试打印uvm_info(get_type_name(), $sformatf(item type is %s, item.get_type_name()), UVM_HIGH)一个“意料之外”的类型名往往比任何调试工具都更快定位问题。get_type_name()返回的是对象创建时由UVM自动记录下来的注册名不是句柄的静态类型。如果发现打印出来的是my_ext_transaction而代码里把它当my_transaction用$cast必然失败思路瞬间清晰。另外一个小技巧是在UVM的do_print里先打印get_type_name()再打印具体字段function void do_print(uvm_printer printer); printer.print_string(type_name, get_type_name()); super.do_print(printer); endfunction这样一个transaction打印出来第一行就是类型避免在庞大的打印信息里找字段时误判类型。6.4 对空句柄保持敏感无论向上转型还是向下转型都要养成判空null check的习惯。代码里加了if (!$cast(...))还要考虑$cast失败之后当前句柄是不是null。有时你继续执行后续代码会让仿真在更靠后的位置报出莫名其妙的空指针光看那一行的报错其实很难倒推是$cast失败导致的。所以建议在每次$cast失败的分支里使用uvm_fatal而不是只是打印一个warning能极大降低现场排查成本。7. 一个可复用的通用代码模板最后给一段遇到父子类句柄问题时可以直接套用的模板都是我在项目里反复使用过的模式。7.1 安全取出子类对象function my_transaction get_my_transaction(uvm_sequence_item item); my_transaction t; if (item null) begin uvm_fatal(NULL_ITEM, input item is null) end if (!$cast(t, item)) begin uvm_fatal(CAST_FAIL, $sformatf(expect my_transaction, get %s, item.get_type_name())) end return t; endfunction这个函数接收一个基类句柄返回一个子类句柄。无论是从sequence、driver还是scoreboard的任意位置只要调它类型不符合就直接fatal不会让问题悄悄扩散。7.2 clone之后取子类字段的模式my_transaction src new(); my_transaction dst; uvm_object tmp; tmp src.clone(); if (!$cast(dst, tmp)) uvm_fatal(CLONE_CAST_FAIL, $sformatf(clone result is %s, tmp.get_type_name()))先把clone结果放进uvm_object再$cast到子类。这样写比直接在clone上做静态转换的结果要安全得多。7.3 处理config_db里读出来的基类句柄uvm_sequence_item got_item; my_sequencer my_seqr; if (uvm_config_db#(uvm_sequence_item)::get(this, , agg_item, got_item)) begin if (!$cast(my_seqr, got_item)) uvm_fatal(CFG_TYPE_FAIL, $sformatf(config item type %s is not my_sequencer, got_item.get_type_name())) end8. 怎么把这套知识讲给团队新人听带团队的时候我一般不像写文档那样罗列语法而是让新人先做一个“句柄/对象”小练习定义两个类父类里有虚方法和一个字段子类里增加一个独有字段然后用父类句柄指向子类对象打印虚方法再直接把子类句柄赋给父类句柄想办法访问独有字段最后把父类句柄$cast回子类句柄。这个练习做完90%的语法问题能原地消化。另一部分人遇到的问题其实不是语法而是拿到UVM源码的时候不知道看哪里。我建议重点读一下uvm_object.svh里的copy、clone、compare、print这几个方法的声明尤其注意它们返回参数和句柄类型再去读uvm_sequence_item.svh里的do_compare、do_copy等宏展开。看过源码之后“父类方法返回父类句柄子类想直接用需要转型”这个UVM特性会变得特别直观。UVM官网和源码的docs部分其实是很好的参考资料。市面上各种UVM实战书里也有专门讲类、继承、多态的章节建议大家动手敲一敲而不是只看不练。单纯记住“父类句柄不能直接访问子类成员”这句话没多大意义遇到实际项目里的复杂层次结构照样会卡壳。多写、多试、多出错才是学习这类知识最有效的路径。遇到奇怪的问题时先打印真实类型再判断是不是句柄和对象的关系出了岔子往往能快速脱离死胡同。
返回列表