在初学uvm和看很多VIP的源码时总是谈callback色变一直觉着这是个高级特性不好理解但随着项目中对callback的使用越来越多callback机制的面纱逐步落下也渐渐体会到了这种机制的妙用因此有必要在这里总结一下。什么是callback机制callback回调机制并非uvm独有它是一种设计模式核心思想是在特定的操作或任务之前或之后插入回调函数。Callback早期使用于C语言。“Callback” 这个英文单词本身直接借用了传统电信领域的“电话回呼Call back”业务。传统场景 你打电话给客服咨询一个复杂问题客服查资料需要半小时。你不可能拿着听筒傻等半小时这叫阻塞/同步。于是你告诉客服“你先查查到了打我这个电话留下一串号码即注册回调。” 客服查完后拨通你的电话触发回调。计算机科学家发现这种非阻塞的、事件驱动的通信方式与软件设计中传递函数地址的行为完美契合因而正式将其命名为 Callback。而在硬件验证领域随着芯片集成度暴增验证语言需要更高的抽象。原本在软件领域用来解耦的 Callback 机制被高级验证语言如 Vera、Specman e以及后来的 SystemVerilog/UVM吸收演变成了验证环境中不可或缺的错误注入Error Injection和动态行为定制的标准工具。systemverilog或者uvm中 callback机制到底实现了什么功能在 SystemVerilog 和 UVM 中Callback回调机制的核心本质是“在不修改原有组件源码的前提下动态向其内部注入或串改行为”。它不仅是一个编程技巧更是解决“通用验证组件如 VIP的固化”与“具体测试用例Testcase的千变万化”之间矛盾的终极武器。callback既保证了验证平台的通用性也实现了验证平台的扩展性具体来说它在验证中主要实现了以下四大核心功能1. 动态错误注入Error Injection—— 最核心的应用在验证芯片时我们不仅要测试它在正常协议下的表现更要测试它遇到异常、协议违规、丢包时能否正确恢复鲁棒性测试。痛点 你的 AXI Driver 是从 Synopsys 买来的 VIP或者是组里前辈写好的通用标准组件源码是加密的或者不允许你直接修改。你无法进到 Driver 源码里去改 WVALID 的拉高时机。Callback 的解法 Driver 内部通常在发送数据前留有 Callback 钩子Hook。你可以通过 Callback 注入一段代码“如果当前是第 100 个包强行将它的 CRC 校验位翻转” 或者 “随机延迟 3 个周期再拉高有效信号”。2. 彻底的解耦与组件重用ReusabilityCallback 实现了真正的开闭原则Open-Closed Principle对扩展开放对修改关闭。没有 Callback 时 如果项目 A 的环境和项目 B 的环境只有 5% 的行为差异比如项目 B 的数据包需要多加一个自定义的 Header你可能不得不把项目 A 的 Monitor 复制一份改名为 monitor_project_b。这导致代码冗余后续维护灾难。有了 Callback 时 Monitor 保持纯净和通用。项目 B 只需要写一个 Callback 类里面包含添加 Header 的逻辑在 Test 层挂载到通用的 Monitor 上即可。同一个 Monitor 组件可以在多个项目中无缝复用。3. 动态的测试行为定制Runtime FlexibilityUVM 的 Factory 机制工厂模式也可以用来替换组件Override但 Factory 是静态且粗暴的替换——你必须在 build_phase 开始前就决定好用组件 B 替换组件 A并且是一整块组件的替换。而 Callback 实现了运行时的动态控制Runtime Control按需开关 你可以在 run_phase 运行到一半时比如仿真到了 100us 之后动态地把某个 Callback 挂载Append上去开始注入错误等测试完了再动态地把它卸载Delete掉。多重叠加 你可以对同一个 Driver 挂载多个不同的 Callback。比如 Callback A 负责“统计发送总数”Callback B 负责“随机延迟”。它们会像流水线一样串联执行Callback Queue而 Factory 替换是无法叠加的。4. 事务级Transaction-level的监控与数据拦截除了修改行为Callback 还可以单纯用来做“旁路监听”。例如在不改动 Scoreboard记分板或 Monitor 内部连线TLM Port的情况下你想单独拉出一个分支来把所有的 Transaction 记录到本地的文本文件中或者同步给一个外部的 C 参考模型Reference Model。你完全可以通过在 Monitor 的 write 或 sample 位置注册一个 Callback 来实现这种数据的“拦截与旁路输出”。在AXI总线项目中就通过callback来实现对trans生命状态的监控QAcallback为什么又叫钩子函数“钩子函数Hook Function” 其实是 Callback回调函数在特定应用场景下的一个形象化称呼。在技术本质上它们俩是一回事都是一段先定义、后执行的代码地址。但之所以被称为“钩子”是因为它在工程上提供了一种极具画面感的隐喻。想象一条工厂的自动化流水线代表程序的主循环或核心骨干流程。物品在传送带上依次前进这个流程是固定的、封装好的你无法轻易暂停或拆卸它。但是工程师在传送带的某些特定位置比如“质检点”、“包装点”预留了几个物理挂钩Hooks。当你开发者有特殊需求时比如当苹果走过来时给它贴个标签你就写好一段“贴标签”的代码然后挂到这个特定的钩子上。当流水线运行到这个位置时系统发现钩子上有东西就会停下来把你的代码执行完给苹果贴标然后再继续往前走。因为它的作用就像是在一条原本连续的执行流中硬生生“钩出”一个时间节点来执行你的自定义代码所以被称为“钩子”。纯systemverilog验证环境中是如何实现callback的在不使用UVM的情况下SystemVerilog原生支持通过抽象类virtual class和继承来实现回调机制。以下是具体方法核心原理在SystemVerilog中回调机制基于面向对象的多态性polymorphism实现定义一个虚方法virtual method的基类作为回调接口用户派生子类并重载override这些虚方法组件在适当位置调用这些虚方法具体实现方式1. 使用抽象类定义回调接口// 定义回调基类抽象接口 virtual class Driver_cbs; // 虚方法默认空实现 virtual task pre_send(Driver drv); // 默认不做任何事 endtask virtual task post_send(Driver drv); // 默认不做任何事 endtask endclass : Driver_cbs2. 在驱动类中集成回调class Driver; // 回调队列存储多个回调对象 Driver_cbs cbs[$]; task send(); // 发送前执行所有回调 foreach (cbs[i]) begin cbs[i].pre_send(this); end // 实际的驱动逻辑 // ... // 发送后执行所有回调 foreach (cbs[i]) begin cbs[i].post_send(this); end endtask : send // 注册回调对象 function void add_callback(Driver_cbs cb); cbs.push_back(cb); endfunction : add_callback endclass : Driver3. 使用者扩展回调类// 扩展回调类实现具体行为 class Driver_cbs_drop extends Driver_cbs; virtual task pre_send(Driver drv); // 自定义行为随机丢弃某些数据包 bit drop; std::randomize(drop) with {drop dist {1:1, 0:9};}; if (drop) begin drv.drop_packet 1; $display(Callback: Dropping packet); end endtask : pre_send endclass : Driver_cbs_drop // 另一个扩展插入错误数据 class Driver_cbs_error extends Driver_cbs; virtual task post_send(Driver drv); // 在发送后修改数据模拟错误 $display(Callback: Injecting error); endtask : post_send endclass : Driver_cbs_error4. 在测试中注册并使用class Test; Driver drv; Driver_cbs_drop drop_cb; Driver_cbs_error error_cb; function new(); drv new(); drop_cb new(); error_cb new(); // 注册回调 drv.add_callback(drop_cb); // 添加丢弃回调 drv.add_callback(error_cb); // 添加错误注入回调 endfunction task run(); repeat(100) begin drv.send(); // 发送时自动调用已注册的回调 end endtask : run endclass : Test总结在不使用UVM的情况下SystemVerilog通过以下方式实现回调1. 划定义规范使用 virtual class 划分好统一的接口规范声明好各种虚方法如 pre_send、post_send。这个基类不实现具体逻辑只负责给底层组件定义可以引用的句柄类型。2. 留预留钩子在底层通用组件如 driver内部声明一个基类类型的队列my_driver_callback cbs[$]。在核心业务流的特定时机通过遍历队列foreach来调用这些虚方法。提供一个注册函数register_cb作为外部代码注入的通道。3. 改编写逻辑在不触动底层组件源码的前提下编写一个具体的派生类继承自该基类。在子类中重写override虚方法将你真正想要的定制逻辑如错误注入、Debug 打印写在里面。4. 挂动态绑定在顶层测试用例test中实例化子类得到真正的对象然后调用底层组件的注册函数把子类对象挂载塞进到底层组件的基类队列中。UVM验证环境中是如何实现callback的 ⭐⭐⭐在 UVMUniversal Verification Methodology中Callback 机制的底层逻辑与前面讲的纯 SystemVerilog多态队列是一样的但 UVM 对其进行了高度的工程化封装。UVM 提供了一套标准的基类、宏和全局管理器uvm_callbacks 居中调度使得不需要自己去写队列和 foreach 遍历并且支持通过字符串在全局动态管理 Callback。UVM验证环境中实现callback机制遵循开发者和使用者两个角色分工的流程。核心组件UVM callback机制涉及以下几个关键类和宏uvm_callback用户自定义回调类的基类uvm_callbacks #(T,CB)用于管理回调注册、删除和迭代的类uvm_register_cb宏注册回调基类与组件的关联uvm_do_callbacks宏在组件特定位置调用回调方法开发者需要做的组件开发者1. 定义回调基类从uvm_callback派生声明虚方法作为回调接口。注意这里定义的是回调基类这个是用来写在固化的验证环境中的所有的回调基类都必须从uvm_callback类扩展。回调基类中必须定义一些虚方法virtual function或者virtual task。一般来说这些虚方法是空的具体实现由使用者对该回调基类进行继承并重写这些虚方法。// 文件: dadd_driver.sv typedef class dadd_driver; class driver_callback extends uvm_callback; uvm_object_utils(driver_callback) function new(string name driver_callback); super.new(name); endfunction : new // 定义虚方法回调接口默认空实现 virtual task pre_send(dadd_driver drv); endtask : pre_send endclass : driver_callback2. 在组件中注册回调类型使用uvm_register_cb宏将回调类与组件类关联。uvm_register_cb(T, CB) 这个宏应该在 预留了 Callback 接口的组件Component类的类定义内部、类名声明之后、任何成员方法之外 调用。通常最标准、最常见的位置是紧跟在组件的 uvm_component_utils(T) 宏的下一行。class dadd_driver extends uvm_driver; uvm_component_utils(dadd_driver) // 注册回调类型将driver_callback与dadd_driver关联 uvm_register_cb(dadd_driver, driver_callback) function new(string name, uvm_component parent); super.new(name, parent); endfunction : new // ... endclass : dadd_driver3. 在适当位置嵌入回调调用点使用uvm_do_callbacks宏在组件的关键位置调用回调方法。task dadd_driver :: main_phase(uvm_phase phase); (posedge vif.clk); forever begin seq_item_port.get_next_item(req); // 在发送前调用回调 uvm_do_callbacks(dadd_driver, driver_callback, pre_send(this)) if(req.data_en) begin vif.mcb.dadd_in_en req.data_en; vif.mcb.dadd_in req.data; vif.mcb.dadd_in_addr req.addr; end else begin vif.mcb.dadd_in_en 0; vif.mcb.dadd_in 0; vif.mcb.dadd_in_addr 0; end (posedge vif.clk); vif.mcb.dadd_in_en 0; vif.mcb.dadd_in 0; vif.mcb.dadd_in_addr 0; seq_item_port.item_done(); end endtask: main_phase使用者需要做的验证工程师1. 扩展回调基类重载回调方法从开发者定义的回调基类派生新类重载需要的虚方法。class dadd_driver_callback extends driver_callback; uvm_object_utils(dadd_driver_callback) function new(string name dadd_driver_callback); super.new(name); endfunction : new // 重载回调方法 virtual task pre_send(dadd_driver drv); int val; std::randomize(val) with { val inside {[0:9]};}; if(val 3) begin drv.req.data 32h5a5a; // 修改数据包内容 end endtask : pre_send endclass : dadd_driver_callback2. 将回调实例添加到目标组件使用uvm_callbacks #(T,CB)::add()函数注册回调实例。class dadd_callback_test extends uvm_test; uvm_component_utils(dadd_callback_test) dadd_environment env; dadd_driver_callback drv_cb; function void build_phase(uvm_phase phase); super.build_phase(phase); env dadd_environment :: type_id :: create(env, this); endfunction : build_phase function void connect_phase(uvm_phase phase); drv_cb dadd_driver_callback :: type_id :: create(drv_cb); // 将回调实例添加到env.iagt.drv组件 uvm_callbacks #(dadd_driver, dadd_driver_callback) :: add(env.iagt.drv, drv_cb); // 显示已注册的回调信息可选 uvm_callbacks #(dadd_driver, dadd_driver_callback) :: display(); endfunction : connect_phase endclass : dadd_callback_test回调执行流程当组件运行到嵌入uvm_do_callbacks的位置时执行流程如下组件调用uvm_do_callbacks宏UVM遍历与该组件实例关联的所有已注册回调对象按注册顺序执行每个回调对象的指定方法如pre_send继续执行组件原有的代码附录Auvm中用户直接接触的与callback相关的类和宏的解析仔细看uvm_callback.svh这个文件会发现这个文件定义了不止uvm_callback这一个类。而是定义了8个类。uvm_typeid_baseuvm_typeiduvm_callbacks_baseuvm_typed_callbacksuvm_callbacks 通过宏调用该类的静态方法uvm_derived_callbacksuvm_callback_iteruvm_callback 回调基类从此类扩展而macros/uvm_callback_defines.svh文件中定义了非常多的宏用来封装上述class中方法的调用。 这两个文件是整个uvm_callback机制运转的核心。而uvm通过封装使得用户在使用callback时只需要和几个简单的类打交道按照固定的流程去执行命令就可以。 而和底层核心原理实现的复杂类uvm并没有暴露给用户初衷也是不想让用户接触。从上面实现uvm_callback的步骤来看无论是开发者还是使用者直接接触到的类只有两个。整个 UVM 的回调机制其实就是围绕 回调对象 ( uvm_callback ) 和 回调管理器 ( uvm_callbacks#(T,CB) 及其父类) 展开的。因此在附录A中将会只针对用户接触的相关类和宏定义进行拆解。而附录B中会针对其他的底层类进一步探索callback机制实现的底层原理。用户直接接触的相关类和宏定义uvm_callback用户自定义回调类的基类uvm_callbacks #(T,CB)用于管理回调注册、删除和迭代的类uvm_register_cb宏注册回调基类与组件的关联uvm_do_callbacks宏在组件特定位置调用回调方法uvm_callback基类uvm_callback继承自uvm_object本身没有什么特别之处回调的禁用与开启callback_mode() 这个函数 callback_mode 是 uvm_callback 类提供的一个开关/控制接口。它的主要作用是动态地启用Enable或禁用Disable某个特定的 Callback 实例或者查询它当前是否处于激活状态。它的设计理念完全模仿了 SystemVerilog 原生自带的 rand_mode() 和 constraint_mode()。在uvm_callback中默认是m_enabled1默认这个callback_mode()函数是不调用的。所以默认情况下都是开启的。1. 查询状态默认不传参即 on -1如果你不传任何参数调用它cb.callback_mode();行为代码会走到第 1392 行的 else 分支打印出当前的使能状态。返回值返回该 Callback 实例更新前/当前的开关状态m_enabled 的值1 代表启用0 代表禁用。2. 关闭这个 Callback传入 on 0调用方式cb.callback_mode(0);行为代码第 1397 行判断成立将内部的控制变量 m_enabled 设为 0。效果这个特定的 Callback 实例被“静音”了。此后即便组件中触发了 uvm_do_callbacks这个实例里的回调方法也不会被执行。3. 开启这个 Callback传入 on 1调用方式cb.callback_mode(1);行为代码第 1401 行判断成立将 m_enabled 设为 1。效果重新激活这个 Callback 实例使其恢复正常工作。实际应用场景为什么需要它在复杂的验证环境中这个函数非常有用尤其是做动态测试Dynamic Testing或阶段性错误注入时。场景定向错误注入控制假设你写了一个 error_cb 实例用于给总线注入“随机单bit错”。你想让这个错误注入只在特定的仿真阶段发生而不是贯穿全过程1. 仿真开始Reset 配置阶段你不希望有错误干扰初始化。my_error_cb.callback_mode(0); // 暂时关闭它2. 仿真中途正常业务流量阶段开始测试 DUT 的纠错能力。#100us; my_error_cb.callback_mode(1); // 动态开启开始注入错误3. 仿真尾声Drain Phase / 扫尾阶段需要清空 DUT 内部的数据不希望再有新错误进去。#500us; my_error_cb.callback_mode(0); // 再次关闭QA这个callback_mode函数如果传入0m_enbalbed0但是在这个赋值之前已经先执行了callback_mode m_enabled此时m_enalbed还是1啊假设当前 Callback 是开启的m_enabled 1你调用了 cb.callback_mode(0)第 1396 行callback_mode m_enabled;此时 m_enabled 确实是 1。所以函数的预备返回值被设置成了 1。第 1398 行m_enabled 0;这时候内部的 m_enabled 正式被改写成了 0关闭成功。第 1405 行endfunction函数执行完毕吐出刚才存好的返回值 1。这种设计是为了实现一个经典的功能返回对象的“历史状态”Previous State。当你想把开关关掉传入 0时UVM 不仅帮你关掉了还顺便通过返回值告诉你“在你关掉我之前我原来是开着的返回 1”。这种“返回旧值”的设计在写测试平台时可以非常优雅地做状态恢复。比如你想临时关闭一下这个 Callback执行完某段代码后再恢复它原本的状态因为你不知道它原本是开着的还是关着的bit bit_old_status; // 1. 关闭 Callback 的同时记录它原本的状态 bit_old_status cb.callback_mode(0); // 2. 执行一些不希望被 Callback 干扰的代码 ... // 3. 事情办完了原样恢复它 cb.callback_mode(bit_old_status);状态指示函数is_enabled()这个函数是一个状态指示函数用来查看某个callback类是否开启了callback模式。QA回调基类定义的虚方法中必须是空的吗不必须。 它可以是完全空的也可以包含默认的、通用的业务逻辑甚至可以做成一个纯虚方法Pure Virtual Method强制子类必须重写。具体怎么写完全取决于你希望这个“默认行为”是什么。在实际项目开发中虚方法的实现通常有以下三种策略1. 空实现最常见如果你希望在默认情况下组件如 Driver不需要任何额外的动作只有在特定测试用例需要时才注入行为那就写成空的。2. 带默认业务逻辑的实现进阶玩法如果你希望这个基类自带一些通用的公共功能比如打日志、计数、基本的合法性检查你完全可以把这些通用代码写在基类的虚方法里。3. 纯虚方法实现强制约束如果你作为平台架构师写了一个专门用于“外部打印数据”的 Callback 基类你希望逼着后续写 Testcase 的新人必须自己去实现具体的打印格式不实现就让它编译报错。这时候可以使用 pure virtualuvm_register_cb宏uvm_register_cb宏本质是调用了 uvm_callbacks 类中的静态函数 m_register_pair()该宏调用时需要传入两个参数分别是两个类的类名注意不是句柄名1. 第一个参数 TTarget Component代表什么目标组件Component的类名。作用指定这个 Callback 机制要绑定到哪一个具体的 UVM 组件比如具体的 Driver、Monitor 或 Scoreboard上。2. 第二个参数 CBCallback Base Class代表什么回调基类的类名。作用指定允许贴到上述组件 T 上的 Callback 类型。这个类本身必须是继承自 uvm_callback 的。注意回调基类必须继承自uvm_callback可以是uvm_callback的子类也可以是孙子类只要是从uvm_callback类继承的就行当你调用 uvm_register_cb(my_driver, my_driver_callback) 时这个宏会利用参数在组件类内部静态地做两件事生成一个唯一的静态变量变量名叫 m_register_cb_my_driver_callback。由于是静态的static它会在仿真最开始的编译/阐述阶段自动初始化。在全局登记这对“CP”通过调用 uvm_callbacks#(my_driver, my_driver_callback)::m_register_pair(my_driver, my_driver_callback)正式在 UVM 底层数据库中把这两个类注册为合法的配对关系。具体m_register_pair的解析详见附录BQAuvm_register_cb宏应该在哪里调用uvm_register_cb(T, CB) 这个宏应该在 预留了 Callback 接口的组件Component类的类定义内部、类名声明之后、任何成员方法之外 调用。通常最标准、最常见的位置是紧跟在组件的 uvm_component_utils(T) 宏的下一行。⚠️ 避坑指南常见错误错误 1误放在 initial 块或 build_phase 内部后果编译报错。因为它是一个宏展开后包含静态声明的语句不能放进任何 function/task 或过程块内部。错误 2误放在类定义的外面比如 endclass 之后后果编译报错。它必须属于组件类的成员作用域内。错误 3Test 派生类需要重复注册吗答案不需要。如果你在 my_driver 里注册了后续你写了一个 class extended_driver extends my_driver子类会自动继承这个 Callback 通道不需要也不应该在子类里重新调用 uvm_register_cb。uvm_do_callbacks宏uvm_do_callbacks宏的参数有三个第一个是要调用callback函数的执行组件类的类名不是句柄名第二个是实现callback函数的类的类名不是句柄名第三个是要执行的callback函数的类的callback方法对uvm_callback原理的最形象解释UVM 的 Callback 就像是一个按“键-值”对Key-Value存放的储物柜。三行关键代码 参数必须保持一致附录BUVM中底层callback的实现原理uvm_callbacks#(T,CB)::m_register_pair(T,CB);
uvm_callback机制学习总结
在初学uvm和看很多VIP的源码时总是谈callback色变一直觉着这是个高级特性不好理解但随着项目中对callback的使用越来越多callback机制的面纱逐步落下也渐渐体会到了这种机制的妙用因此有必要在这里总结一下。什么是callback机制callback回调机制并非uvm独有它是一种设计模式核心思想是在特定的操作或任务之前或之后插入回调函数。Callback早期使用于C语言。“Callback” 这个英文单词本身直接借用了传统电信领域的“电话回呼Call back”业务。传统场景 你打电话给客服咨询一个复杂问题客服查资料需要半小时。你不可能拿着听筒傻等半小时这叫阻塞/同步。于是你告诉客服“你先查查到了打我这个电话留下一串号码即注册回调。” 客服查完后拨通你的电话触发回调。计算机科学家发现这种非阻塞的、事件驱动的通信方式与软件设计中传递函数地址的行为完美契合因而正式将其命名为 Callback。而在硬件验证领域随着芯片集成度暴增验证语言需要更高的抽象。原本在软件领域用来解耦的 Callback 机制被高级验证语言如 Vera、Specman e以及后来的 SystemVerilog/UVM吸收演变成了验证环境中不可或缺的错误注入Error Injection和动态行为定制的标准工具。systemverilog或者uvm中 callback机制到底实现了什么功能在 SystemVerilog 和 UVM 中Callback回调机制的核心本质是“在不修改原有组件源码的前提下动态向其内部注入或串改行为”。它不仅是一个编程技巧更是解决“通用验证组件如 VIP的固化”与“具体测试用例Testcase的千变万化”之间矛盾的终极武器。callback既保证了验证平台的通用性也实现了验证平台的扩展性具体来说它在验证中主要实现了以下四大核心功能1. 动态错误注入Error Injection—— 最核心的应用在验证芯片时我们不仅要测试它在正常协议下的表现更要测试它遇到异常、协议违规、丢包时能否正确恢复鲁棒性测试。痛点 你的 AXI Driver 是从 Synopsys 买来的 VIP或者是组里前辈写好的通用标准组件源码是加密的或者不允许你直接修改。你无法进到 Driver 源码里去改 WVALID 的拉高时机。Callback 的解法 Driver 内部通常在发送数据前留有 Callback 钩子Hook。你可以通过 Callback 注入一段代码“如果当前是第 100 个包强行将它的 CRC 校验位翻转” 或者 “随机延迟 3 个周期再拉高有效信号”。2. 彻底的解耦与组件重用ReusabilityCallback 实现了真正的开闭原则Open-Closed Principle对扩展开放对修改关闭。没有 Callback 时 如果项目 A 的环境和项目 B 的环境只有 5% 的行为差异比如项目 B 的数据包需要多加一个自定义的 Header你可能不得不把项目 A 的 Monitor 复制一份改名为 monitor_project_b。这导致代码冗余后续维护灾难。有了 Callback 时 Monitor 保持纯净和通用。项目 B 只需要写一个 Callback 类里面包含添加 Header 的逻辑在 Test 层挂载到通用的 Monitor 上即可。同一个 Monitor 组件可以在多个项目中无缝复用。3. 动态的测试行为定制Runtime FlexibilityUVM 的 Factory 机制工厂模式也可以用来替换组件Override但 Factory 是静态且粗暴的替换——你必须在 build_phase 开始前就决定好用组件 B 替换组件 A并且是一整块组件的替换。而 Callback 实现了运行时的动态控制Runtime Control按需开关 你可以在 run_phase 运行到一半时比如仿真到了 100us 之后动态地把某个 Callback 挂载Append上去开始注入错误等测试完了再动态地把它卸载Delete掉。多重叠加 你可以对同一个 Driver 挂载多个不同的 Callback。比如 Callback A 负责“统计发送总数”Callback B 负责“随机延迟”。它们会像流水线一样串联执行Callback Queue而 Factory 替换是无法叠加的。4. 事务级Transaction-level的监控与数据拦截除了修改行为Callback 还可以单纯用来做“旁路监听”。例如在不改动 Scoreboard记分板或 Monitor 内部连线TLM Port的情况下你想单独拉出一个分支来把所有的 Transaction 记录到本地的文本文件中或者同步给一个外部的 C 参考模型Reference Model。你完全可以通过在 Monitor 的 write 或 sample 位置注册一个 Callback 来实现这种数据的“拦截与旁路输出”。在AXI总线项目中就通过callback来实现对trans生命状态的监控QAcallback为什么又叫钩子函数“钩子函数Hook Function” 其实是 Callback回调函数在特定应用场景下的一个形象化称呼。在技术本质上它们俩是一回事都是一段先定义、后执行的代码地址。但之所以被称为“钩子”是因为它在工程上提供了一种极具画面感的隐喻。想象一条工厂的自动化流水线代表程序的主循环或核心骨干流程。物品在传送带上依次前进这个流程是固定的、封装好的你无法轻易暂停或拆卸它。但是工程师在传送带的某些特定位置比如“质检点”、“包装点”预留了几个物理挂钩Hooks。当你开发者有特殊需求时比如当苹果走过来时给它贴个标签你就写好一段“贴标签”的代码然后挂到这个特定的钩子上。当流水线运行到这个位置时系统发现钩子上有东西就会停下来把你的代码执行完给苹果贴标然后再继续往前走。因为它的作用就像是在一条原本连续的执行流中硬生生“钩出”一个时间节点来执行你的自定义代码所以被称为“钩子”。纯systemverilog验证环境中是如何实现callback的在不使用UVM的情况下SystemVerilog原生支持通过抽象类virtual class和继承来实现回调机制。以下是具体方法核心原理在SystemVerilog中回调机制基于面向对象的多态性polymorphism实现定义一个虚方法virtual method的基类作为回调接口用户派生子类并重载override这些虚方法组件在适当位置调用这些虚方法具体实现方式1. 使用抽象类定义回调接口// 定义回调基类抽象接口 virtual class Driver_cbs; // 虚方法默认空实现 virtual task pre_send(Driver drv); // 默认不做任何事 endtask virtual task post_send(Driver drv); // 默认不做任何事 endtask endclass : Driver_cbs2. 在驱动类中集成回调class Driver; // 回调队列存储多个回调对象 Driver_cbs cbs[$]; task send(); // 发送前执行所有回调 foreach (cbs[i]) begin cbs[i].pre_send(this); end // 实际的驱动逻辑 // ... // 发送后执行所有回调 foreach (cbs[i]) begin cbs[i].post_send(this); end endtask : send // 注册回调对象 function void add_callback(Driver_cbs cb); cbs.push_back(cb); endfunction : add_callback endclass : Driver3. 使用者扩展回调类// 扩展回调类实现具体行为 class Driver_cbs_drop extends Driver_cbs; virtual task pre_send(Driver drv); // 自定义行为随机丢弃某些数据包 bit drop; std::randomize(drop) with {drop dist {1:1, 0:9};}; if (drop) begin drv.drop_packet 1; $display(Callback: Dropping packet); end endtask : pre_send endclass : Driver_cbs_drop // 另一个扩展插入错误数据 class Driver_cbs_error extends Driver_cbs; virtual task post_send(Driver drv); // 在发送后修改数据模拟错误 $display(Callback: Injecting error); endtask : post_send endclass : Driver_cbs_error4. 在测试中注册并使用class Test; Driver drv; Driver_cbs_drop drop_cb; Driver_cbs_error error_cb; function new(); drv new(); drop_cb new(); error_cb new(); // 注册回调 drv.add_callback(drop_cb); // 添加丢弃回调 drv.add_callback(error_cb); // 添加错误注入回调 endfunction task run(); repeat(100) begin drv.send(); // 发送时自动调用已注册的回调 end endtask : run endclass : Test总结在不使用UVM的情况下SystemVerilog通过以下方式实现回调1. 划定义规范使用 virtual class 划分好统一的接口规范声明好各种虚方法如 pre_send、post_send。这个基类不实现具体逻辑只负责给底层组件定义可以引用的句柄类型。2. 留预留钩子在底层通用组件如 driver内部声明一个基类类型的队列my_driver_callback cbs[$]。在核心业务流的特定时机通过遍历队列foreach来调用这些虚方法。提供一个注册函数register_cb作为外部代码注入的通道。3. 改编写逻辑在不触动底层组件源码的前提下编写一个具体的派生类继承自该基类。在子类中重写override虚方法将你真正想要的定制逻辑如错误注入、Debug 打印写在里面。4. 挂动态绑定在顶层测试用例test中实例化子类得到真正的对象然后调用底层组件的注册函数把子类对象挂载塞进到底层组件的基类队列中。UVM验证环境中是如何实现callback的 ⭐⭐⭐在 UVMUniversal Verification Methodology中Callback 机制的底层逻辑与前面讲的纯 SystemVerilog多态队列是一样的但 UVM 对其进行了高度的工程化封装。UVM 提供了一套标准的基类、宏和全局管理器uvm_callbacks 居中调度使得不需要自己去写队列和 foreach 遍历并且支持通过字符串在全局动态管理 Callback。UVM验证环境中实现callback机制遵循开发者和使用者两个角色分工的流程。核心组件UVM callback机制涉及以下几个关键类和宏uvm_callback用户自定义回调类的基类uvm_callbacks #(T,CB)用于管理回调注册、删除和迭代的类uvm_register_cb宏注册回调基类与组件的关联uvm_do_callbacks宏在组件特定位置调用回调方法开发者需要做的组件开发者1. 定义回调基类从uvm_callback派生声明虚方法作为回调接口。注意这里定义的是回调基类这个是用来写在固化的验证环境中的所有的回调基类都必须从uvm_callback类扩展。回调基类中必须定义一些虚方法virtual function或者virtual task。一般来说这些虚方法是空的具体实现由使用者对该回调基类进行继承并重写这些虚方法。// 文件: dadd_driver.sv typedef class dadd_driver; class driver_callback extends uvm_callback; uvm_object_utils(driver_callback) function new(string name driver_callback); super.new(name); endfunction : new // 定义虚方法回调接口默认空实现 virtual task pre_send(dadd_driver drv); endtask : pre_send endclass : driver_callback2. 在组件中注册回调类型使用uvm_register_cb宏将回调类与组件类关联。uvm_register_cb(T, CB) 这个宏应该在 预留了 Callback 接口的组件Component类的类定义内部、类名声明之后、任何成员方法之外 调用。通常最标准、最常见的位置是紧跟在组件的 uvm_component_utils(T) 宏的下一行。class dadd_driver extends uvm_driver; uvm_component_utils(dadd_driver) // 注册回调类型将driver_callback与dadd_driver关联 uvm_register_cb(dadd_driver, driver_callback) function new(string name, uvm_component parent); super.new(name, parent); endfunction : new // ... endclass : dadd_driver3. 在适当位置嵌入回调调用点使用uvm_do_callbacks宏在组件的关键位置调用回调方法。task dadd_driver :: main_phase(uvm_phase phase); (posedge vif.clk); forever begin seq_item_port.get_next_item(req); // 在发送前调用回调 uvm_do_callbacks(dadd_driver, driver_callback, pre_send(this)) if(req.data_en) begin vif.mcb.dadd_in_en req.data_en; vif.mcb.dadd_in req.data; vif.mcb.dadd_in_addr req.addr; end else begin vif.mcb.dadd_in_en 0; vif.mcb.dadd_in 0; vif.mcb.dadd_in_addr 0; end (posedge vif.clk); vif.mcb.dadd_in_en 0; vif.mcb.dadd_in 0; vif.mcb.dadd_in_addr 0; seq_item_port.item_done(); end endtask: main_phase使用者需要做的验证工程师1. 扩展回调基类重载回调方法从开发者定义的回调基类派生新类重载需要的虚方法。class dadd_driver_callback extends driver_callback; uvm_object_utils(dadd_driver_callback) function new(string name dadd_driver_callback); super.new(name); endfunction : new // 重载回调方法 virtual task pre_send(dadd_driver drv); int val; std::randomize(val) with { val inside {[0:9]};}; if(val 3) begin drv.req.data 32h5a5a; // 修改数据包内容 end endtask : pre_send endclass : dadd_driver_callback2. 将回调实例添加到目标组件使用uvm_callbacks #(T,CB)::add()函数注册回调实例。class dadd_callback_test extends uvm_test; uvm_component_utils(dadd_callback_test) dadd_environment env; dadd_driver_callback drv_cb; function void build_phase(uvm_phase phase); super.build_phase(phase); env dadd_environment :: type_id :: create(env, this); endfunction : build_phase function void connect_phase(uvm_phase phase); drv_cb dadd_driver_callback :: type_id :: create(drv_cb); // 将回调实例添加到env.iagt.drv组件 uvm_callbacks #(dadd_driver, dadd_driver_callback) :: add(env.iagt.drv, drv_cb); // 显示已注册的回调信息可选 uvm_callbacks #(dadd_driver, dadd_driver_callback) :: display(); endfunction : connect_phase endclass : dadd_callback_test回调执行流程当组件运行到嵌入uvm_do_callbacks的位置时执行流程如下组件调用uvm_do_callbacks宏UVM遍历与该组件实例关联的所有已注册回调对象按注册顺序执行每个回调对象的指定方法如pre_send继续执行组件原有的代码附录Auvm中用户直接接触的与callback相关的类和宏的解析仔细看uvm_callback.svh这个文件会发现这个文件定义了不止uvm_callback这一个类。而是定义了8个类。uvm_typeid_baseuvm_typeiduvm_callbacks_baseuvm_typed_callbacksuvm_callbacks 通过宏调用该类的静态方法uvm_derived_callbacksuvm_callback_iteruvm_callback 回调基类从此类扩展而macros/uvm_callback_defines.svh文件中定义了非常多的宏用来封装上述class中方法的调用。 这两个文件是整个uvm_callback机制运转的核心。而uvm通过封装使得用户在使用callback时只需要和几个简单的类打交道按照固定的流程去执行命令就可以。 而和底层核心原理实现的复杂类uvm并没有暴露给用户初衷也是不想让用户接触。从上面实现uvm_callback的步骤来看无论是开发者还是使用者直接接触到的类只有两个。整个 UVM 的回调机制其实就是围绕 回调对象 ( uvm_callback ) 和 回调管理器 ( uvm_callbacks#(T,CB) 及其父类) 展开的。因此在附录A中将会只针对用户接触的相关类和宏定义进行拆解。而附录B中会针对其他的底层类进一步探索callback机制实现的底层原理。用户直接接触的相关类和宏定义uvm_callback用户自定义回调类的基类uvm_callbacks #(T,CB)用于管理回调注册、删除和迭代的类uvm_register_cb宏注册回调基类与组件的关联uvm_do_callbacks宏在组件特定位置调用回调方法uvm_callback基类uvm_callback继承自uvm_object本身没有什么特别之处回调的禁用与开启callback_mode() 这个函数 callback_mode 是 uvm_callback 类提供的一个开关/控制接口。它的主要作用是动态地启用Enable或禁用Disable某个特定的 Callback 实例或者查询它当前是否处于激活状态。它的设计理念完全模仿了 SystemVerilog 原生自带的 rand_mode() 和 constraint_mode()。在uvm_callback中默认是m_enabled1默认这个callback_mode()函数是不调用的。所以默认情况下都是开启的。1. 查询状态默认不传参即 on -1如果你不传任何参数调用它cb.callback_mode();行为代码会走到第 1392 行的 else 分支打印出当前的使能状态。返回值返回该 Callback 实例更新前/当前的开关状态m_enabled 的值1 代表启用0 代表禁用。2. 关闭这个 Callback传入 on 0调用方式cb.callback_mode(0);行为代码第 1397 行判断成立将内部的控制变量 m_enabled 设为 0。效果这个特定的 Callback 实例被“静音”了。此后即便组件中触发了 uvm_do_callbacks这个实例里的回调方法也不会被执行。3. 开启这个 Callback传入 on 1调用方式cb.callback_mode(1);行为代码第 1401 行判断成立将 m_enabled 设为 1。效果重新激活这个 Callback 实例使其恢复正常工作。实际应用场景为什么需要它在复杂的验证环境中这个函数非常有用尤其是做动态测试Dynamic Testing或阶段性错误注入时。场景定向错误注入控制假设你写了一个 error_cb 实例用于给总线注入“随机单bit错”。你想让这个错误注入只在特定的仿真阶段发生而不是贯穿全过程1. 仿真开始Reset 配置阶段你不希望有错误干扰初始化。my_error_cb.callback_mode(0); // 暂时关闭它2. 仿真中途正常业务流量阶段开始测试 DUT 的纠错能力。#100us; my_error_cb.callback_mode(1); // 动态开启开始注入错误3. 仿真尾声Drain Phase / 扫尾阶段需要清空 DUT 内部的数据不希望再有新错误进去。#500us; my_error_cb.callback_mode(0); // 再次关闭QA这个callback_mode函数如果传入0m_enbalbed0但是在这个赋值之前已经先执行了callback_mode m_enabled此时m_enalbed还是1啊假设当前 Callback 是开启的m_enabled 1你调用了 cb.callback_mode(0)第 1396 行callback_mode m_enabled;此时 m_enabled 确实是 1。所以函数的预备返回值被设置成了 1。第 1398 行m_enabled 0;这时候内部的 m_enabled 正式被改写成了 0关闭成功。第 1405 行endfunction函数执行完毕吐出刚才存好的返回值 1。这种设计是为了实现一个经典的功能返回对象的“历史状态”Previous State。当你想把开关关掉传入 0时UVM 不仅帮你关掉了还顺便通过返回值告诉你“在你关掉我之前我原来是开着的返回 1”。这种“返回旧值”的设计在写测试平台时可以非常优雅地做状态恢复。比如你想临时关闭一下这个 Callback执行完某段代码后再恢复它原本的状态因为你不知道它原本是开着的还是关着的bit bit_old_status; // 1. 关闭 Callback 的同时记录它原本的状态 bit_old_status cb.callback_mode(0); // 2. 执行一些不希望被 Callback 干扰的代码 ... // 3. 事情办完了原样恢复它 cb.callback_mode(bit_old_status);状态指示函数is_enabled()这个函数是一个状态指示函数用来查看某个callback类是否开启了callback模式。QA回调基类定义的虚方法中必须是空的吗不必须。 它可以是完全空的也可以包含默认的、通用的业务逻辑甚至可以做成一个纯虚方法Pure Virtual Method强制子类必须重写。具体怎么写完全取决于你希望这个“默认行为”是什么。在实际项目开发中虚方法的实现通常有以下三种策略1. 空实现最常见如果你希望在默认情况下组件如 Driver不需要任何额外的动作只有在特定测试用例需要时才注入行为那就写成空的。2. 带默认业务逻辑的实现进阶玩法如果你希望这个基类自带一些通用的公共功能比如打日志、计数、基本的合法性检查你完全可以把这些通用代码写在基类的虚方法里。3. 纯虚方法实现强制约束如果你作为平台架构师写了一个专门用于“外部打印数据”的 Callback 基类你希望逼着后续写 Testcase 的新人必须自己去实现具体的打印格式不实现就让它编译报错。这时候可以使用 pure virtualuvm_register_cb宏uvm_register_cb宏本质是调用了 uvm_callbacks 类中的静态函数 m_register_pair()该宏调用时需要传入两个参数分别是两个类的类名注意不是句柄名1. 第一个参数 TTarget Component代表什么目标组件Component的类名。作用指定这个 Callback 机制要绑定到哪一个具体的 UVM 组件比如具体的 Driver、Monitor 或 Scoreboard上。2. 第二个参数 CBCallback Base Class代表什么回调基类的类名。作用指定允许贴到上述组件 T 上的 Callback 类型。这个类本身必须是继承自 uvm_callback 的。注意回调基类必须继承自uvm_callback可以是uvm_callback的子类也可以是孙子类只要是从uvm_callback类继承的就行当你调用 uvm_register_cb(my_driver, my_driver_callback) 时这个宏会利用参数在组件类内部静态地做两件事生成一个唯一的静态变量变量名叫 m_register_cb_my_driver_callback。由于是静态的static它会在仿真最开始的编译/阐述阶段自动初始化。在全局登记这对“CP”通过调用 uvm_callbacks#(my_driver, my_driver_callback)::m_register_pair(my_driver, my_driver_callback)正式在 UVM 底层数据库中把这两个类注册为合法的配对关系。具体m_register_pair的解析详见附录BQAuvm_register_cb宏应该在哪里调用uvm_register_cb(T, CB) 这个宏应该在 预留了 Callback 接口的组件Component类的类定义内部、类名声明之后、任何成员方法之外 调用。通常最标准、最常见的位置是紧跟在组件的 uvm_component_utils(T) 宏的下一行。⚠️ 避坑指南常见错误错误 1误放在 initial 块或 build_phase 内部后果编译报错。因为它是一个宏展开后包含静态声明的语句不能放进任何 function/task 或过程块内部。错误 2误放在类定义的外面比如 endclass 之后后果编译报错。它必须属于组件类的成员作用域内。错误 3Test 派生类需要重复注册吗答案不需要。如果你在 my_driver 里注册了后续你写了一个 class extended_driver extends my_driver子类会自动继承这个 Callback 通道不需要也不应该在子类里重新调用 uvm_register_cb。uvm_do_callbacks宏uvm_do_callbacks宏的参数有三个第一个是要调用callback函数的执行组件类的类名不是句柄名第二个是实现callback函数的类的类名不是句柄名第三个是要执行的callback函数的类的callback方法对uvm_callback原理的最形象解释UVM 的 Callback 就像是一个按“键-值”对Key-Value存放的储物柜。三行关键代码 参数必须保持一致附录BUVM中底层callback的实现原理uvm_callbacks#(T,CB)::m_register_pair(T,CB);